Section 20.2. Synchronization


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 helps the developer avoid having a second thread barge in on your object until the first thread is finished with it.

This section examines three synchronization mechanisms: the Interlock class, the C# lock statement, and the Monitor class. But first, you need to create a shared resource, (often a file or printer); in this case a simple integer variable: counter. You will 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 you might open a file, manipulate its contents, and then close it, here you 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 reads the value of counter (0) and assigns that to a temporary variable. It then increments the temporary variable. While it is doing its work, the second thread reads the value of counter (still 0) and assigns 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, you'll see 1,2,3,3,4,4. Example 20-3 shows the complete source code and output for this example.

Example 20-3. Simulating a shared resource
#region Using directives using System; using System.Collections.Generic; using System.Text; using System.Threading; #endregion namespace SharedResource {    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

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 the 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 the CLR 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(0);          // display the incremented value          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 what you need.


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 C# lock feature.

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 to an object, and follow the keyword with a statement block:

lock(expression) statement-block

For example, you can modify Incrementer 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.

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 presumed to be 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 and the developer calls Pulse (discussed in a bit). 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 counts up to 10. The decrementer method counts 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 are 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.

When a thread is finished with the monitor, it must 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
#region Using directives using System; using System.Collections.Generic; using System.Text; using System.Threading; #endregion namespace UsingAMonitor {    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) Building. NET Applications with C#
Programming C#: Building .NET Applications with C#
ISBN: 0596006993
EAN: 2147483647
Year: 2003
Pages: 180
Authors: Jesse Liberty

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