20.2 Synchronization

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.

This section examines three synchronization mechanisms provided by the CLR: the Interlock class, the C# 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:

int counter = 0;

Modify the Incrementer method to increment the counter member variable:

public void Incrementer( ) {    try    {       while (counter < 1000)       {          int temp = counter;          temp++; // increment          // simulate some work in this method          Thread.Sleep(1);          // assign the Incremented value          // to the counter variable          // and display the results          counter = temp;          Console.WriteLine(             "Thread {0}. Incrementer: {1}",              Thread.CurrentThread.Name,             counter);       }    }

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, sleep for one millisecond to simulate work, 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.

Example 20-3. Simulating a shared resource
namespace Programming_CSharp {    using System;    using System.Threading;    class Tester    {       private int counter = 0;       static void Main( )       {          // make an instance of this class          Tester t = new Tester( );          // run outside static Main          t.DoTest( );                        }       public void DoTest( )       {          Thread t1 = new Thread( new ThreadStart(Incrementer) );          t1.IsBackground=true;          t1.Name = "ThreadOne";          t1.Start( );          Console.WriteLine("Started thread {0}",             t1.Name);          Thread t2 = new Thread( new ThreadStart(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.");       }       // demo function, counts up to 1K       public void Incrementer( )       {          try          {             while (counter < 1000)             {                int temp = counter;                temp++; // increment                // simulate some work in this method                Thread.Sleep(1);                // assign the decremented value                // and display the results                counter = temp;                Console.WriteLine(                   "Thread {0}. Incrementer: {1}",                    Thread.CurrentThread.Name,                   counter);             }          }          catch (ThreadInterruptedException)          {             Console.WriteLine(                "Thread {0} interrupted! Cleaning up...",                Thread.CurrentThread.Name);          }          finally          {             Console.WriteLine(                "Thread {0} Exiting. ",                Thread.CurrentThread.Name);          }       }    } } Output: Started thread ThreadOne Started thread ThreadTwo Thread ThreadOne. Incrementer: 1 Thread ThreadOne. Incrementer: 2 Thread ThreadOne. Incrementer: 3 Thread ThreadTwo. 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

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 C# 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.).

20.2.1 Using Interlocked

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 C# 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 void Incrementer( ) {    try    {       while (counter < 1000)       {          int temp = Interlocked.Increment(ref counter);          // simulate some work in this method          Thread.Sleep(1);          // assign the decremented value          // and display the results          Console.WriteLine(             "Thread {0}. Incrementer: {1}",              Thread.CurrentThread.Name,             temp);       }    } }

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 int. Because int values are passed by value, use the ref keyword, as described in Chapter 4.

The Increment( ) method is overloaded and can take a reference to a long rather than to an int, 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 ThreadOne. Incrementer: 5 Thread ThreadTwo. Incrementer: 6 Thread ThreadOne. Incrementer: 7 Thread ThreadTwo. Incrementer: 8 Thread ThreadOne. Incrementer: 9 Thread ThreadTwo. Incrementer: 10 Thread ThreadOne. Incrementer: 11 Thread ThreadTwo. Incrementer: 12 Thread ThreadOne. Incrementer: 13 Thread ThreadTwo. Incrementer: 14 Thread ThreadOne. Incrementer: 15 Thread ThreadTwo. Incrementer: 16 Thread ThreadOne. Incrementer: 17 Thread ThreadTwo. Incrementer: 18 Thread ThreadOne. Incrementer: 19 Thread ThreadTwo. Incrementer: 20

20.2.2 Using Locks

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 Lock object.

A lock 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 Lock is to request a lock on an object and then to execute a statement or block of statements. The lock is removed at the end of the statement block.

C# provides direct support for locks through the lock keyword. Pass in a reference object and follow the keyword with a statement block:

lock(expression) statement-block

For example, you can modify Incrementer once again to use a lock statement, as follows:

public void Incrementer( ) {    try    {       while (counter < 1000)       {          int temp;          lock (this)          {             temp = counter;             temp ++;             Thread.Sleep(1);             counter = temp;          }          // assign the decremented value          // and display the results          Console.WriteLine(             "Thread {0}. Incrementer: {1}",              Thread.CurrentThread.Name,             temp);       }    }

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.

20.2.3 Using Monitors

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(this);

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) {     Monitor.Wait(this); }

This call to Wait( ) frees the monitor, but signals 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(this);

Pulse( ) signals 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(this);

Example 20-4 continues the simulation, providing synchronized access to a counter variable using a Monitor.

Example 20-4. Using a Monitor object
namespace Programming_CSharp {    using System;    using System.Threading;    class Tester    {       private long counter = 0;       static void Main( )       {          // make an instance of this class          Tester t = new Tester( );          // run outside static Main          t.DoTest( );                        }       public void DoTest( )       {          // create an array of unnamed threads          Thread[] myThreads =              {                new Thread( new ThreadStart(Decrementer) ),                new Thread( new ThreadStart(Incrementer) )                           };          // start each thread          int ctr = 1;          foreach (Thread myThread in myThreads)          {             myThread.IsBackground=true;             myThread.Start( );             myThread.Name = "Thread" + ctr.ToString( );             ctr++;             Console.WriteLine("Started thread {0}", myThread.Name);             Thread.Sleep(50);          }          // wait for all threads to end before continuing          foreach (Thread myThread in myThreads)          {             myThread.Join( );          }          // after all threads end, print a message          Console.WriteLine("All my threads are done.");       }       void Decrementer( )       {          try           {             // synchronize this area of code             Monitor.Enter(this);             // if counter is not yet 10             // then free the monitor to other waiting             // threads, but wait in line for your turn             if (counter < 10)             {                Console.WriteLine(                   "[{0}] In Decrementer. Counter: {1}. Gotta Wait!",                   Thread.CurrentThread.Name, counter);                Monitor.Wait(this);             }             while (counter >0)             {                long temp = counter;                temp--;                Thread.Sleep(1);                counter = temp;                Console.WriteLine(                   "[{0}] In Decrementer. Counter: {1}. ",                   Thread.CurrentThread.Name, counter);             }          }          finally          {             Monitor.Exit(this);          }       }       void Incrementer( )       {          try          {             Monitor.Enter(this);             while (counter < 10)             {                long temp = counter;                temp++;                Thread.Sleep(1);                counter = temp;                Console.WriteLine(                   "[{0}] In Incrementer. Counter: {1}",                   Thread.CurrentThread.Name, counter);             }             // I'm done incrementing for now, let another             // thread have the Monitor             Monitor.Pulse(this);          }          finally          {             Console.WriteLine("[{0}] Exiting...",                Thread.CurrentThread.Name);             Monitor.Exit(this);          }       }    } } Output: Started thread Thread1 [Thread1] In Decrementer. Counter: 0. Gotta Wait! Started thread Thread2 [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... [Thread1] In Decrementer. Counter: 9. [Thread1] In Decrementer. Counter: 8. [Thread1] In Decrementer. Counter: 7. [Thread1] In Decrementer. Counter: 6. [Thread1] In Decrementer. 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. All my threads are done.

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:

void Incrementer( ) {    try    {       while (counter < 10)       {        Monitor.Enter(this);         long temp = counter;          temp++;          Thread.Sleep(1);          counter = temp;          Console.WriteLine(             "[{0}] In Incrementer. Counter: {1}",             Thread.CurrentThread.Name, counter);          Monitor.Pulse(this);          Monitor.Exit(this);       }

Rewrite Decrementer as well, changing the if statement to a while statement and knocking down the value from 10 to 5:

//if (counter < 10) while (counter < 5)

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:

[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


Programming C#
C# Programming: From Problem Analysis to Program Design
ISBN: 1423901460
EAN: 2147483647
Year: 2003
Pages: 182
Authors: Barbara Doyle

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net