only for RuBoard - do not distribute or recompile |
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 could get output like this:
abcdabcdefghijklmnopqrsefghijklmnopqrstuvwxyztuvwxyz
Thread synchronization comprises techniques for ensuring that multiple threads coordinate their access to shared resources.
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:
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz
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 the later section Section 3.8.3):
System.Threading.Monitor.Enter(expression); try { ... } finally { System.Threading.Monitor.Exit(expression); }
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 scope of the lock statement block. 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.
The MonitorTest example actually contains a bug. When you run the program, it prints the correct output, but then the console window locks up. This occurs because the last thread printing z 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. This situation is known as a deadlock. Most common deadlock situations can be avoided by ensuring that you always acquire resources in the same order.
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 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.
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.
The System.Threading.Monitor class provides an implementation of Hoare's Monitor that allows you to use any reference-type instance as a monitor.
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.
The TryEnter methodsare similar to the Enter method but 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.
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.
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.
only for RuBoard - do not distribute or recompile |