Overview of Synchronization Primitives
The .NET Framework provides a range of synchronization primitives for controlling the interactions of threads and avoiding race conditions. These can be roughly divided into three categories: locking, signaling, and interlocked operations.
The categories are not tidy and clearly defined: some synchronization mechanisms have characteristics of multiple categories; events that release a single thread at a time are functionally like locks; the release of any lock can be thought of as a signal; and interlocked operations can be used to construct locks. However, the categories are still useful.
It is important to remember that thread synchronization is cooperative. If even one thread bypasses a synchronization mechanism and accesses the protected resource directly, that synchronization mechanism cannot be effective.
Locks give control of a resource to one thread at a time, or to a specified number of threads. A thread that requests an exclusive lock when the lock is in use blocks until the lock becomes available.
The simplest form of locking is the C# lock statement (SyncLock in Visual Basic), which controls access to a block of code. Such a block is frequently referred to as a critical section. The lock statement is implemented by using the Enter and Exit methods of the Monitor class, and it uses try…catch…finally to ensure that the lock is released.
In general, using the lock statement to protect small blocks of code, never spanning more than a single method, is the best way to use the Monitor class. Although powerful, the Monitor class is prone to orphan locks and deadlocks.
The Monitor class provides additional functionality, which can be used in conjunction with the lock statement:
The TryEnter method allows a thread that is blocked waiting for the resource to give up after a specified interval. It returns a Boolean value indicating success or failure, which can be used to detect and avoid potential deadlocks.
The Wait method is called by a thread in a critical section. It gives up control of the resource and blocks until the resource is available again.
Timeouts on Wait method overloads allow waiting threads to escape to the ready queue.
The Monitor class is not instantiable. Its methods are static (Shared in Visual Basic), and act on an instantiable lock object.
For a conceptual overview, see Monitors.
Threads request a Mutex by calling an overload of its WaitOne method. Overloads with timeouts are provided, to allow threads to give up the wait. Unlike the Monitor class, a mutex can be either local or global. Global mutexes, also called named mutexes, are visible throughout the operating system, and can be used to synchronize threads in multiple application domains or processes. Local mutexes derive from MarshalByRefObject, and can be used across application domain boundaries.
For a conceptual overview, see Mutexes.
Locks need not be exclusive. It is often useful to allow a limited number of threads concurrent access to a resource. Semaphores and reader-writer locks are designed to control this kind of pooled resource access.
The ReaderWriterLockSlim class addresses the case where a thread that changes data, the writer, must have exclusive access to a resource. When the writer is not active, any number of readers can access the resource (for example, by calling the EnterReadLock method). When a thread requests exclusive access, (for example, by calling the EnterWriteLock method), subsequent reader requests block until all existing readers have exited the lock, and the writer has entered and exited the lock.
ReaderWriterLockSlim has thread affinity.
For a conceptual overview, see Reader-Writer Locks.
The Semaphore class allows a specified number of threads to access a resource. Additional threads requesting the resource block until a thread releases the semaphore.
For a conceptual overview, see Semaphores.
The simplest way to wait for a signal from another thread is to call the Join method, which blocks until the other thread completes. Join has two overloads that allow the blocked thread to break out of the wait after a specified interval has elapsed.
Wait handles provide a much richer set of waiting and signaling capabilities.
Wait handles derive from the WaitHandle class, which in turn derives from MarshalByRefObject. Thus, wait handles can be used to synchronize the activities of threads across application domain boundaries.
Threads block on wait handles by calling the instance method WaitOne or one of the static methods WaitAll, WaitAny, or SignalAndWait. How they are released depends on which method was called, and on the kind of wait handles.
For a conceptual overview, see Wait Handles.
Event Wait Handles
Event wait handles include the EventWaitHandle class and its derived classes, AutoResetEvent and ManualResetEvent. Threads are released from an event wait handle when the event wait handle is signaled by calling its Set method or by using the SignalAndWait method.
Event wait handles either reset themselves automatically, like a turnstile that allows only one thread through each time it is signaled, or must be reset manually, like a gate that is closed until signaled and then open until someone closes it. As their names imply, AutoResetEvent and ManualResetEvent represent the former and latter, respectively.
Event wait handles do not have thread affinity. Any thread can signal an event wait handle.
For a conceptual overview, see EventWaitHandle, AutoResetEvent, and ManualResetEvent.
Mutex and Semaphore Classes
Because the Mutex and Semaphore classes derive from WaitHandle, they can be used with the static methods of WaitHandle. For example, a thread can use the WaitAll method to wait until all three of the following are true: an EventWaitHandle is signaled, a Mutex is released, and a Semaphore is released. Similarly, a thread can use the WaitAny method to wait until any one of those conditions is true.
For a Mutex or a Semaphore, being signaled means being released. If either type is used as the first argument of the SignalAndWait method, it is released. In the case of a Mutex, which has thread affinity, an exception is thrown if the calling thread does not own the mutex. As noted previously, semaphores do not have thread affinity.
Interlocked operations are simple atomic operations performed on a memory location by static methods of the Interlocked class. Those atomic operations include addition, increment and decrement, exchange, conditional exchange depending on a comparison, and read operations for 64-bit values on 32-bit platforms.
The guarantee of atomicity is limited to individual operations; when multiple operations must be performed as a unit, a more coarse-grained synchronization mechanism must be used.
Although none of these operations are locks or signals, they can be used to construct locks and signals. Because they are native to the Windows operating system, interlocked operations are extremely fast.
Interlocked operations can be used with volatile memory guarantees to write applications which exhibit powerful non-blocking concurrency, however they require sophisticated, low level programming, and for most purposes simple locks are a better choice.
For a conceptual overview, see Interlocked Operations.