At times, you might want to control access to a resource, such as an object's properties or methods, so that only one thread at a time can modify or use that resource. Your object is similar to the airplane restroom discussed earlier, and the various threads are like the people waiting in line. Synchronization is provided by a lock on the object, which prevents a second thread from barging in on your object until the first thread is finished with it.
In this section you examine three synchronization mechanisms provided by the CLR: the Interlock class, the Visual Basic .NET Lock statement, and the Monitor class. But first, you need to simulate a shared resource, such as a file or printer, with a simple integer variable: counter. Rather than opening the file or accessing the printer, you'll increment counter from each of two threads.
To start, declare the member variable and initialize it to 0:
Private counter As Integer = 0
Modify the Incrementer method to increment the counter member variable:
Public Sub Incrementer( ) Try While counter < 1000 Dim temp As Integer = counter temp += 1 ' increment Thread.Sleep(0) counter = temp Console.WriteLine("Thread {0}. Incrementer: {1}", _ Thread.CurrentThread.Name, counter) End While
The idea here is to simulate the work that might be done with a controlled resource. Just as we might open a file, manipulate its contents, and then close it, here we read the value of counter into a temporary variable, increment the temporary variable, suspend the thread to simulate work (Thread.Sleep(0)), and then assign the incremented value back to counter.
The problem is that your first thread will read the value of counter (0) and assign that to a temporary variable. It will then increment the temporary variable. While it is doing its work, the second thread will read the value of counter (still 0) and assign that value to a temporary variable. The first thread finishes its work, then assigns the temporary value (1) back to counter and displays it. The second thread does the same. What is printed is 1,1. In the next go around, the same thing happens. Rather than having the two threads count 1,2,3,4, we see 1,1,2,2,3,3. Example 20-3 shows the complete source code and output for this example.
Option Strict On Imports System Imports System.Threading Class Tester Private counter As Integer = 0 Shared Sub Main( ) ' make an instance of this class Dim t As New Tester( ) ' run outside static Main t.DoTest( ) End Sub 'Main Public Sub DoTest( ) Dim t1 As New Thread(New ThreadStart(AddressOf Incrementer)) t1.IsBackground = True t1.Name = "ThreadOne" t1.Start( ) Console.WriteLine("Started thread {0}", t1.Name) Dim t2 As New Thread(New ThreadStart(AddressOf Incrementer)) t2.IsBackground = True t2.Name = "ThreadTwo" t2.Start( ) Console.WriteLine("Started thread {0}", t2.Name) t1.Join( ) t2.Join( ) ' after all threads end, print a message Console.WriteLine("All my threads are done.") End Sub 'DoTest ' demo function, counts up to 1K Public Sub Incrementer( ) Try While counter < 1000 Dim temp As Integer = counter temp += 1 ' increment ' simulate some work in this method Thread.Sleep(0) ' assign the decremented value ' and display the results counter = temp Console.WriteLine("Thread {0}. Incrementer: {1}", _ Thread.CurrentThread.Name, counter) End While Catch e As ThreadInterruptedException Console.WriteLine("Thread {0} interrupted! Cleaning up...", _ Thread.CurrentThread.Name) Finally Console.WriteLine("Thread {0} Exiting. ", _ Thread.CurrentThread.Name) End Try End Sub 'Incrementer End Class 'Tester Output (excerpt) : Started thread ThreadOne Started thread ThreadTwo Thread ThreadTwo. Incrementer: 1 Thread ThreadOne. Incrementer: 1 Thread ThreadTwo. Incrementer: 2 Thread ThreadOne. Incrementer: 2 Thread ThreadTwo. Incrementer: 3 Thread ThreadOne. Incrementer: 3 Thread ThreadTwo. Incrementer: 4 Thread ThreadOne. Incrementer: 4 Thread ThreadTwo. Incrementer: 5 Thread ThreadOne. Incrementer: 5 Thread ThreadTwo. Incrementer: 6 Thread ThreadOne. Incrementer: 6 Thread ThreadTwo. Incrementer: 7 Thread ThreadOne. Incrementer: 7 Thread ThreadTwo. Incrementer: 8 Thread ThreadOne. Incrementer: 8 Thread ThreadTwo. Incrementer: 9 Thread ThreadOne. Incrementer: 9
Assume your two threads are accessing a database record rather than reading a member variable. For example, your code might be part of an inventory system for a book retailer. A customer asks if Programming Visual Basic .NET is available. The first thread reads the value and finds that there is one book on hand. The customer wants to buy the book, so the thread proceeds to gather credit card information and validate the customer's address.
While this is happening, a second thread asks if this wonderful book is still available. The first thread has not yet updated the record, so one book still shows as available. The second thread begins the purchase process. Meanwhile, the first thread finishes and decrements the counter to zero. The second thread, blissfully unaware of the activity of the first, also sets the value back to zero. Unfortunately, you have now sold the same copy of the book twice.
As noted earlier, you need to synchronize access to the counter object (or to the database record, file, printer, etc.).
The CLR provides a number of synchronization mechanisms. These include the common synchronization tools such as critical sections (called Locks in .NET), as well as more sophisticated tools such as a Monitor class. Each is discussed later in this chapter.
Incrementing and decrementing a value is such a common programming pattern, and one which so often needs synchronization protection, that Visual Basic .NET offers a special class, Interlocked, just for this purpose. Interlocked has two methods, Increment and Decrement, which not only increment or decrement a value, but also do so under synchronization control.
Modify the Incrementer method from Example 20-3 as follows:
Public Sub Incrementer( ) Try While counter < 1000 Interlocked.Increment(counter) Console.WriteLine("Thread {0}. Incrementer: {1}", _ Thread.CurrentThread.Name, counter) Thread.Sleep(0) End While
The catch and finally blocks and the remainder of the program are unchanged from the previous example.
Interlocked.Increment( ) expects a single parameter: a reference to an Integer. The Increment( ) method is overloaded and can take a reference to a long, rather than to an Integer, if that is more convenient.
Once this change is made, access to the counter member is synchronized, and the output is what we'd expect.
Output (excerpts): Started thread ThreadOne Started thread ThreadTwo Thread ThreadOne. Incrementer: 1 Thread ThreadTwo. Incrementer: 2 Thread ThreadOne. Incrementer: 3 Thread ThreadTwo. Incrementer: 4 Thread ThreadTwo. Incrementer: 5 Thread ThreadOne. Incrementer: 6 Thread ThreadTwo. Incrementer: 7 Thread ThreadOne. Incrementer: 8 Thread ThreadTwo. Incrementer: 9 Thread ThreadOne. Incrementer: 10
Although the Interlocked object is fine if you want to increment or decrement a value, there will be times when you want to control access to other objects as well. What is needed is a more general synchronization mechanism. This is provided by the .NET SyncLock object.
A SyncLock marks a critical section of your code, providing synchronization to an object you designate while the lock is in effect. The syntax of using a SyncLock statement is to request a SyncLock on an object and then to execute a statement or block of statements. The SyncLock is removed at the end of the statement block.
Visual Basic .NET provides direct support for locks through the SyncLock keyword. For example, you can modify Incrementer once again to use a lock statement, as follows:
Public Sub Incrementer( ) Try While counter < 1000 SyncLock Me Dim temp As Integer = counter temp += 1 Thread.Sleep(0) counter = temp Console.WriteLine("Thread {0}. Incrementer: {1}", _ Thread.CurrentThread.Name, counter) End SyncLock End While
The catch and finally blocks and the remainder of the program are unchanged from the previous example.
The output from this code is identical to that produced using Interlocked.
The objects used so far will be sufficient for most needs. For the most sophisticated control over resources, you might want to use a monitor. A monitor lets you decide when to enter and exit the synchronization, and it lets you wait for another area of your code to become free.
A monitor acts as a smart lock on a resource. When you want to begin synchronization, call the Enter( ) method of the monitor, passing in the object you want to lock:
Monitor.Enter(Me)
If the monitor is unavailable, the object protected by the monitor is in use. You can do other work while you wait for the monitor to become available and then try again. You can also explicitly choose to Wait( ), suspending your thread until the moment the monitor is free. Wait( ) helps you control thread ordering.
For example, suppose you are downloading and printing an article from the Web. For efficiency, you'd like to print in a background thread, but you want to ensure that at least 10 pages have downloaded before you begin.
Your printing thread will wait until the get-file thread signals that enough of the file has been read. You don't want to Join the get-file thread because the file might be hundreds of pages. You don't want to wait until it has completely finished downloading, but you do want to ensure that at least 10 pages have been read before your print thread begins. The Wait( ) method is just the ticket.
To simulate this, rewrite Tester and add back the decrementer method. Your incrementer will count up to 10. The decrementer method will count down to zero. It turns out you don't want to start decrementing unless the value of counter is at least 5.
In Decrementer, call Enter on the monitor. Then check the value of counter, and if it is less than 5, call Wait on the monitor:
If counter < 5 Then Console.WriteLine( _ "[{0}] In Decrementer. Counter: {1}. Gotta Wait!", _ Thread.CurrentThread.Name, counter) Monitor.Wait(Me) End If
This call to Wait( ) frees the monitor, but signals to the CLR that you want the monitor back the next time it is free. Waiting threads will be notified of a chance to run again if the active thread calls Pulse( ):
Monitor.Pulse(Me)
Pulse( ) signals to the CLR that there has been a change in state that might free a thread that is waiting. The CLR will keep track of the fact that the earlier thread asked to wait, and threads will be guaranteed access in the order in which the waits were requested. ("Your wait is important to us and will be handled in the order received.")
When a thread is finished with the monitor, it can mark the end of its controlled area of code with a call to Exit( ):
Monitor.Exit(Me)
Example 20-4 continues the simulation, providing synchronized access to a counter variable using a Monitor.
Option Strict On Imports System Imports System.Threading Class Tester Shared Sub Main( ) ' make an instance of this class Dim t As New Tester( ) ' run outside static Main t.DoTest( ) End Sub 'Main Public Sub DoTest( ) ' create an array of unnamed threads Dim myThreads As Thread( ) = _ {New Thread(New ThreadStart(AddressOf Decrementer)), _ New Thread(New ThreadStart(AddressOf Incrementer))} ' start each thread Dim ctr As Integer = 1 Dim myThread As Thread For Each myThread In myThreads myThread.IsBackground = True myThread.Start( ) myThread.Name = "Thread" & ctr.ToString( ) ctr += 1 Console.WriteLine("Started thread {0}", myThread.Name) Thread.Sleep(50) Next myThread ' wait for all threads to end before continuing Dim t As Thread For Each t In myThreads t.Join( ) Next t ' after all threads end, print a message Console.WriteLine("All my threads are done.") End Sub 'DoTest Sub Decrementer( ) Try ' synchronize this area of code Monitor.Enter(Me) ' if counter is not yet 5 ' then free the monitor to other waiting ' threads, but wait in line for your turn If counter < 5 Then Console.WriteLine( _ "[{0}] In Decrementer. Counter: {1}. Gotta Wait!", _ Thread.CurrentThread.Name, counter) Monitor.Wait(Me) End If While counter > 0 Dim temp As Long = counter temp -= 1 Thread.Sleep(0) counter = temp Console.WriteLine("[{0}] In Decrementer. Counter: {1}. ", _ Thread.CurrentThread.Name, counter) End While Finally Monitor.Exit(Me) End Try End Sub 'Decrementer Sub Incrementer( ) Try Monitor.Enter(Me) While counter < 10 Dim temp As Long = counter temp += 1 Thread.Sleep(0) counter = temp Console.WriteLine("[{0}] In Incrementer. Counter: {1}", _ Thread.CurrentThread.Name, counter) End While ' I'm done incrementing for now, let another ' thread have the Monitor Monitor.Pulse(Me) Finally Console.WriteLine("[{0}] Exiting...", _ Thread.CurrentThread.Name) Monitor.Exit(Me) End Try End Sub 'Incrementer Private counter As Long = 0 End Class 'Tester
In this example, Decrementer is started first. In the output you see Thread1 (the decrementer) start up and then realize that it has to wait. You then see Thread2 start up. Only when Thread2 pulses does Thread1 begin its work.
Try some experiments with this code. First, comment out the call to Pulse( ). You'll find that Thread1 never resumes. Without Pulse( ) there is no signal to the waiting threads.
As a second experiment, rewrite Incrementer to pulse and exit the monitor after each increment:
Sub Incrementer( ) Try While counter < 10 Monitor.Enter(Me) Dim temp As Long = counter temp += 1 Thread.Sleep(0) counter = temp Console.WriteLine("[{0}] In Incrementer. Counter: {1}", _ Thread.CurrentThread.Name, counter) Monitor.Pulse(Me) Monitor.Exit(Me) End While
Rewrite Decrementer as well, changing the If statement to a While statement and knocking down the value from 10 to 5:
While counter < 5 Console.WriteLine( _ "[{0}] In Decrementer. Counter: {1}. Gotta Wait!", _ Thread.CurrentThread.Name, counter) Monitor.Wait(Me) End While
The net effect of these two changes is to cause Thread2, the Incrementer, to pulse the Decrementer after each increment. While the value is smaller than five, the Decrementer must continue to wait; once the value goes over five, the Decrementer runs to completion. When it is done, the Incrementer thread can run again. The output is shown here:
Started thread Thread1 [Thread1] In Decrementer. Counter: 0. Gotta Wait! Started thread Thread2 [Thread2] In Incrementer. Counter: 1 [Thread1] In Decrementer. Counter: 1. Gotta Wait! [Thread2] In Incrementer. Counter: 2 [Thread1] In Decrementer. Counter: 2. Gotta Wait! [Thread2] In Incrementer. Counter: 3 [Thread1] In Decrementer. Counter: 3. Gotta Wait! [Thread2] In Incrementer. Counter: 4 [Thread1] In Decrementer. Counter: 4. Gotta Wait! [Thread2] In Incrementer. Counter: 5 [Thread1] In Decrementer. Counter: 4. [Thread1] In Decrementer. Counter: 3. [Thread1] In Decrementer. Counter: 2. [Thread1] In Decrementer. Counter: 1. [Thread1] In Decrementer. Counter: 0. [Thread2] In Incrementer. Counter: 1 [Thread2] In Incrementer. Counter: 2 [Thread2] In Incrementer. Counter: 3 [Thread2] In Incrementer. Counter: 4 [Thread2] In Incrementer. Counter: 5 [Thread2] In Incrementer. Counter: 6 [Thread2] In Incrementer. Counter: 7 [Thread2] In Incrementer. Counter: 8 [Thread2] In Incrementer. Counter: 9 [Thread2] In Incrementer. Counter: 10 [Thread2] Exiting... All my threads are done.
Top |