4.8 Synchronize the Execution of Multiple Threads


Problem

You need to coordinate the activities of multiple threads to ensure the efficient use of shared resources and that you do not corrupt shared data when a thread context switch occurs during an operation that changes the data.

Solution

Use the Monitor, AutoResetEvent, ManualResetEvent , and Mutex classes from the System.Threading namespace.

Discussion

The greatest challenge in writing a multithreaded application is ensuring that the threads work in concert. This is commonly referred to as thread synchronization and includes

  • Ensuring threads access shared objects and data correctly so that they do not cause corruption.

  • Ensuring threads execute only when they are meant to and cause minimum overhead when they are idle.

The most commonly used synchronization mechanism is the Monitor class. The Monitor class allows a single thread to obtain an exclusive lock on an object by calling the static method Monitor.Enter . By acquiring an exclusive lock prior to accessing a shared resource or data, you ensure that only one thread can access the resource concurrently. Once the thread has finished with the resource, release the lock to allow another thread to access it. A block of code that enforces this behavior is often referred to as a critical section .

You can use any object to act as the lock, and it's common to use the keyword this in order to obtain a lock on the current object. The key point is that all threads attempting to access a shared resource must try to acquire the same lock. Other threads that attempt to acquire a lock on the same object will block (enter a WaitSleepJoin state) and are added to the lock's ready queue until the thread that owns the lock releases it by calling the static method Monitor.Exit . When the owning thread calls Exit , one of the threads from the ready queue acquires the lock. If the owner of a lock does not release it by calling Exit , all other threads will block indefinitely. Therefore, it's important to place the Exit call within a finally block to ensure that it's called even if an exception occurs.

Because Monitor is used so frequently in multithreaded applications, C# provides language-level support through the lock statement, which the compiler translates to use of the Monitor class. A block of code encapsulated in a lock statement is equivalent to calling Monitor.Enter when entering the block, and Monitor.Exit when exiting the block. In addition, the compiler automatically places the Monitor.Exit call in a finally block to ensure that the lock is released if an exception is thrown.

The thread that currently owns the lock can call Monitor.Wait , which will release the lock and place the calling thread on the lock's wait queue . Threads in a wait queue also have a state of WaitSleepJoin and will continue to block until a thread that owns the lock calls either of the Pulse or PulseAll methods of the Monitor class. Pulse moves one of the waiting threads from the wait queue to the ready queue, whereas PulseAll moves all threads. Once a thread has moved from the wait queue to the ready queue, it can acquire the lock next time it's released. It's important to understand that threads on a lock's wait queue will not acquire a released lock; they will wait indefinitely until you call Pulse or PulseAll to move them to the ready queue. The use of Wait and Pulse is a common approach when a pool of threads is used to process work items from a shared queue.

The ThreadSyncExample class shown here demonstrates the use of both the Monitor class and the lock statement. The example starts three threads that each (in turn ) acquire the lock to an object named consoleGate . Each thread then calls Monitor.Wait . When the user presses the Enter key the first time, Monitor.Pulse is called to release one waiting thread. The second time the user presses Enter, Monitor.PulseAll is called, releasing all remaining waiting threads.

 using System; using System.Threading; public class ThreadSyncExample {     // Declare a static object to use for locking in static methods      // because there is no access to 'this'.     private static object consoleGate = new Object();     private static void DisplayMessage() {         Console.WriteLine("{0} : Thread started, acquiring lock...",             DateTime.Now.ToString("HH:mm:ss.ffff"));         // Acquire a lock on the consoleGate object.         try {             Monitor.Enter(consoleGate);             Console.WriteLine("{0} : {1}",                 DateTime.Now.ToString("HH:mm:ss.ffff"),                 "Acquired consoleGate lock, waiting...");             // Wait until Pulse is called on the consoleGate object.             Monitor.Wait(consoleGate);             Console.WriteLine("{0} : Thread pulsed, terminating.",                 DateTime.Now.ToString("HH:mm:ss.ffff"));         } finally {             Monitor.Exit(consoleGate);         }     }     public static void Main() {         // Acquire a lock on the consoleGate object.         lock (consoleGate) {             // Create and start three new threads running the              // DisplayMesssage method.             for (int count = 0; count < 3; count++) {                 (new Thread(new ThreadStart(DisplayMessage))).Start();             }         }         Thread.Sleep(1000);         // Wake up a single waiting thread.         Console.WriteLine("{0} : {1}",              DateTime.Now.ToString("HH:mm:ss.ffff"),             "Press Enter to pulse one waiting thread.");         Console.ReadLine();         // Acquire a lock on the consoleGate object.         lock (consoleGate) {                          // Pulse 1 waiting thread.             Monitor.Pulse(consoleGate);         }         // Wake up all waiting threads.         Console.WriteLine("{0} : {1}",               DateTime.Now.ToString("HH:mm:ss.ffff"),             "Press Enter to pulse all waiting threads.");         Console.ReadLine();         // Acquire a lock on the consoleGate object.         lock (consoleGate) {                          // Pulse all waiting threads.             Monitor.PulseAll(consoleGate);         }         // Wait to continue.         Console.WriteLine("Main method complete. Press Enter.");         Console.ReadLine();     } } 

Other classes commonly used to provide synchronization between threads are the subclasses of the System.Threading.WaitHandle class. These include the AutoResetEvent , ManualResetEvent , and Mutex . Instances of these classes can be in either a signaled or unsignaled state. Threads can use the methods of the classes listed in Table 4.2 (inherited from the WaitHandle class) to enter a WaitSleepJoin state and wait for the state of one or more WaitHandle - derived objects to become signaled.

Table 4.2: WaitHandl e Methods for Synchronizing Thread Execution

Method

Description

WaitAny

A static method that causes the calling thread to enter a WaitSleepJoin state and wait for any one of the WaitHandle objects in a WaitHandle array to be signaled. You can also specify a time-out value.

WaitAll

A static method that causes the calling thread to enter a WaitSleepJoin state and wait for all the WaitHandle objects in a WaitHandle array to be signaled. You can also specify a time-out value. The WaitAllExample method in recipe 4.2 demonstrates the use of the WaitAll method.

WaitOne

Causes the calling thread to enter a WaitSleepJoin state and wait for a specific WaitHandle object to be signaled. The WaitingExample method in recipe 4.2 demonstrates the use of the WaitOne method.

The key differences between the AutoResetEvent , ManualResetEvent , and Mutex classes are how they transition from a signaled to an unsignaled state, and their visibility. The AutoResetEvent and ManualResetEvent classes are local to a process. To signal an AutoResetEvent , call its Set method, which will release only one thread that is waiting on the event. The AutoResetEvent will automatically return to an unsignaled state. The example in recipe 4.4 demonstrates the use of an AutoResetEvent class.

The ManualResetEvent class must be manually switched back and forth between signaled to unsignaled using its Set and Reset methods. Calling Set on a ManualResetEvent will set it to a signaled state, releasing all threads that are waiting on the event. Only by calling Reset does the ManualResetEvent become unsignaled.

A Mutex is signaled when it's not owned by any thread. A thread acquires ownership of the Mutex either at construction or by using one of the methods listed in Table 4.2. Ownership of the Mutex is released by calling the Mutex.ReleaseMutex method, which signals the Mutex and allows another thread to gain ownership. The key benefit of a Mutex is that you can use them to synchronize threads across process boundaries; recipe 4.12 demonstrates the use of a Mutex .

Aside from the functionality already described, a key difference between the WaitHandle classes and the Monitor class is that Monitor is implemented completely in managed code, whereas the WaitHandle classes provide wrappers around operating system primitives. This has the following consequences:

  • Use of the Monitor class means your code is more portable because you are not dependent on the capabilities of the underlying operating system.

  • You can use the classes derived from WaitHandle to synchronize the execution of both managed and unmanaged threads, whereas Monitor can synchronize only managed threads.




C# Programmer[ap]s Cookbook
C# Programmer[ap]s Cookbook
ISBN: 735619301
EAN: N/A
Year: 2006
Pages: 266

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