Threading

A C# application runs in one or more threads that effectively execute in parallel within the same application. Here is a simple multithreaded application:

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

In this example, a new thread object is constructed by passing it a ThreadStart delegate that wraps the method that specifies where to start execution for that thread. You then start the thread and call Go, so two separate threads are running Go in parallel. However, there’s a problem: both threads share a common resource—the console. If you run ThreadTest, you get output something like this:

abcdabcdefghijklmnopqrsefghjiklmnopqrstuvwxyztuvwxyz

Thread Synchronization

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

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 BCL Monitor class (see Section 3.8.3):

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

Pulse and Wait operations

In combination with locks, the next most common threading operations are Pulse and Wait. 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. The second way is to call the Wait method, which temporarily releases the lock on an object and makes the thread fall asleep until another thread wakes it up by pulsing the object.

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. You can 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.

Atomic operations

Atomic operations are operations the system promises will not be interrupted. In the previous examples, the method Go 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 allows basic operations to be performed without requiring a lock. This can be useful, since acquiring a lock is many times slower than a simple atomic operation.

Common Thread Types

Much of the functionality of threads is provided through the classes in the System.Threading namespace. The most basic thread class to understand is the Monitor class, which is explained in the following section.

Monitor Class

The System.Threading.Monitor class provides an implementation of Hoare’s Monitor that allows you to use any reference-type instance as a monitor.

Enter and Exit methods

The Enter and Exit methods, respectively, obtain and release a lock on an object. If the object is already held by another thread, Enter waits until the lock is released, or the thread is interrupted by a ThreadInterruptedException . Every call to Enter for a given object on a thread should be matched with a call to Exit for the same object on the same thread.

TryEnter methods

The TryEnter methods are similar to the Enter method, but they don’t require a lock on the object to proceed. These methods return true if the lock is obtained, and false if it isn’t, optionally passing in a timeout parameter that specifies the maximum time to wait for the other threads to relinquish the lock.

Wait methods

The thread holding a lock on an object may call one of the Wait methods to temporarily release the lock and block itself, while it waits for another thread to notify it by executing a pulse on the monitor. This approach can tell a worker thread that there is work to perform on that object. The overloaded versions of Wait allow you to specify a timeout that reactivates the thread if a pulse hasn’t arrived within the specified duration. When the thread wakes up, it reacquires the monitor for the object (potentially blocking until the monitor becomes available). Wait returns true if the thread is reactivated by another thread pulsing the monitor and returns false if the Wait call times out without receiving a pulse.

Pulse and PulseAll methods

A thread holding a lock on an object may call Pulse on that object to wake up a blocked thread as soon as the thread calling Pulse has released its lock on the monitor. If multiple threads are waiting on the same monitor, Pulse activates only the first in the queue (successive calls to Pulse wake up other waiting threads, one per call). The PulseAll method successively wakes up all the threads.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.20.90