DekGenius.com
[ Team LiB ] Previous Section Next Section

16.1 Thread Synchronization

Thread synchronization comprises techniques for ensuring that multiple threads coordinate their access to shared resources.

16.1.1 The lock Statement

C# provides the lock statement to ensure that only one thread at a time can access a block of code. Consider the following example:

using System;
using System.Threading;
class LockTest {
  static void Main( ) {
    LockTest lt = new LockTest ( );
    Thread t = new Thread(new ThreadStart(lt.Go));
    t.Start( );
    lt.Go( );
  }
  void Go( ) {
    lock(this)
      for ( char c='a'; c<='z'; c++)
        Console.Write(c);
  }
}

Running LockTest produces the following output:

abcdefghijklmnopqrstuvwzyzabcdefghijklmnopqrstuvwzyz

The lock statement acquires a lock on any reference type instance. If another thread has already acquired the lock, the thread doesn't continue until the other thread relinquishes its lock on that instance.

The lock statement is actually a syntactic shortcut for calling the Enter( ) and Exit( ) methods of the FCL Monitor class (see Section 16.2.1):

System.Threading.Monitor.Enter(expression);
try {
  ...
}
finally {
  System.Threading.Monitor.Exit(expression);
}

16.1.2 Pulse and Wait Operations

The next most common threading operations are Pulse and Wait in combination with locks. These operations let threads communicate with each other via a monitor that maintains a list of threads waiting to grab an object's lock:

using System;
using System.Threading;
class MonitorTest {
  static void Main( ) {
    MonitorTest mt = new MonitorTest( );
    Thread t = new Thread(new ThreadStart(mt.Go));
    t.Start( );
    mt.Go( );
  }
  void Go( ) {
    for ( char c='a'; c<='z'; c++)
      lock(this) {
        Console.Write(c);
        Monitor.Pulse(this);
        Monitor.Wait(this);
      }
  }
}

Running MonitorTest produces the following result:

aabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz

The Pulse method tells the monitor to wake up the next thread that is waiting to get a lock on that object as soon as the current thread has released it. The current thread typically releases the monitor in one of two ways. First, execution may leave the lock statement blocked. Alternatively, calling the Wait method temporarily releases the lock on an object and makes the thread fall asleep until another thread wakes it up by pulsing the object.

16.1.3 Deadlocks

The MonitorTest example actually contains a type of bug called a deadlock. When you run the program, it prints the correct output, but then the console window locks up. This is because there are two sleeping threads, and neither will wake the other. The deadlock occurs because when printing z, each thread goes to sleep but never gets pulsed. Solve the problem by replacing the Go method with this new implementation:

  void Go( ) {
    for ( char c='a'; c<='z'; c++)
      lock(this) {
        Console.Write(c);
        Monitor.Pulse(this);
        if (c<'z')
          Monitor.Wait(this);
      }
  }

In general, the danger of using locks is that two threads may both end up being blocked waiting for a resource held by the other thread. Most common deadlock situations can be avoided by ensuring that you always acquire resources in the same order.

16.1.4 Atomic Operations

Atomic operations are operations the system promises will not be interrupted. In the previous examples, the Go method isn't atomic, because it can be interrupted while it is running so another thread can run. However, updating a variable is atomic, because the operation is guaranteed to complete without control being passed to another thread. The Interlocked class provides additional atomic operations, which allow basic operations to perform without requiring a lock. This can be useful, since acquiring a lock is many times slower than a simple atomic operation.

    [ Team LiB ] Previous Section Next Section