Mutexes, Semaphores, Monitors, ReaderWriterLocks, AutoResetEvents & ManualResets oh my!

Mutexes, Semaphores, Monitors, ReaderWriterLocks, AutoResetEvents & ManualResets oh my!

Up until a few years ago, mainstream programming for the most part did not involve asynchrony and/or parallelism except for the most performance demanding applications. In the multi core world we live in today async and parallel programming have become common place. Libraries such as TPL which provide first class framework level support and keywords like async/await built on TPL provide language support for writing elegant asynchronous code. Win8 Metro style apps built on the WinRT programming model is inherently async to help keep Metro style UIs responsive and fluid. As easy as it might appear to write async programs using these new features we should not forget our roots and the primitive constructs in the .NET framwework that TPL uses behind the scenes. This post provides a quick primer on the synchronization primitives that exist in the framework and the use cases of ech of them.

Mutex
A mutex is a synchronization primitive that provides a "locking mechanism" so that only a single thread access to a resource.Other threads can request access to the resource by calling WaitOne() and will be blocked until the thread onwning the Mutex releases it via a call to Release(). The thread holding the Mutex can request the same Mutex multiple times(typically via recursion) by calling WaitOne() without blocking but must call Release() the same number of times. A Mutex enforces thread identity i.e. only the thread holding the Mutex can release it. Mutexes can be named or unnamed. An unnamed Mutex is scoped to the appdomain it is running in, whereas a named Mutex can span across app domain / process boundaries.

Example:

  1. class MutexSample   
  2. {   
  3.     private static void RunMutex()   
  4.     {   
  5.         Mutex mutex = new Mutex(true);   
  6.         Console.WriteLine("Main thread holds mutex");   
  7.         ThreadPool.QueueUserWorkItem(_ =>   
  8.         {   
  9.             Console.WriteLine("Thread {0} waiting to acquire mutex", Thread.CurrentThread.ManagedThreadId);   
  10.             mutex.WaitOne();   
  11.             Console.WriteLine("Thread {0} acquired mutex", Thread.CurrentThread.ManagedThreadId);   
  12.             Thread.Sleep(2000);   
  13.             Console.WriteLine("Thread {0} released mutex", Thread.CurrentThread.ManagedThreadId);   
  14.             mutex.ReleaseMutex();   
  15.         }   
  16.         );   
  17.         Thread.Sleep(5000);   
  18.         mutex.ReleaseMutex();   
  19.         Console.WriteLine("Main thread released mutex");   
  20.         Console.ReadLine();   
  21.     }   
  22.   
  23.     private static void RunNamedMutex()   
  24.     {   
  25.         Mutex mutex = new Mutex(false"MYNAMEDMUTEX");   
  26.         Console.WriteLine("Main thread waiting to acquire mutex");   
  27.         mutex.WaitOne();   
  28.         Console.WriteLine("Main thread holds mutex");   
  29.   
  30.         Thread.Sleep(8000);   
  31.         mutex.ReleaseMutex();   
  32.         Console.WriteLine("Main thread released mutex");   
  33.         Console.ReadLine();   
  34.     }   
  35.     static void Main(string[] args)   
  36.     {   
  37.      //Mutexes enforce  ThreadID, a thread  that owns a mutex can acquire it repeatedly using Waitone() without blocking but must call Release   
  38.      //same # of times   
  39.       RunMutex();   
  40.       RunNamedMutex();   
  41.     }    
  42. }  

Semaphore
A semaphore is a synchronization primitive that provides a "signalling mechanism" to control access to a pool of resources. A semaphore is count based where the count represents the maximum number of resources available which corresponds to the number of threads that can enter the Semaphore.Threads can enter a Semaphore by calling WaitOne() which decreases the count and exits it by calling Release() which increases the count. When the count reaches zero subsequent requests to WaitOne() will block.Unlike a Mutex which uses a ThreadId to maintain ownership, a Semaphore has no notion of "ownership" or ThreadIds. This can lead to situations where a programming error can cause a thread that holds on to the Semaphore to calls Release() more times than it should,so use caution! Like a Mutex, a Semaphore can be named or unnamed. An unnamed Semaphore is scoped to the appdomain it is running in, whereas a named Mutex spans across appdomain / process boundaries.

Example:

  1. class SemaphoreSample   
  2. {   
  3.     private static void RunNamedSemaphore()   
  4.     {   
  5.         Semaphore sem = new Semaphore(1, 1, "MYNAMEDSEMAPHORE");   
  6.         Console.WriteLine("Main Thread holds onto all 3 available entries");   
  7.         sem.WaitOne();   
  8.         Console.WriteLine("Acquired semaphore");   
  9.         Thread.Sleep(8000);   
  10.         sem.Release();   
  11.         Console.WriteLine("Released semaphore");   
  12.         Console.ReadLine();   
  13.     }   
  14.   
  15.     private static void RunUnNamedSemaphore()   
  16.     {   
  17.         Semaphore sem = new Semaphore(0, 3);   
  18.         Console.WriteLine("Main Thread holds onto all 3 available entries");   
  19.         for (int i = 0; i < 5; i++)   
  20.         {   
  21.   
  22.   
  23.             ThreadPool.QueueUserWorkItem(_ =>   
  24.                     {   
  25.                         Console.WriteLine("Thread {0} waiting to acquire semaphore", Thread.CurrentThread.ManagedThreadId);   
  26.                         sem.WaitOne();   
  27.                         Console.WriteLine("Thread {0} acquired semaphore", Thread.CurrentThread.ManagedThreadId);   
  28.                         Thread.Sleep(2000);   
  29.                         sem.Release();   
  30.                         Console.WriteLine("Thread {0} releases semaphore", Thread.CurrentThread.ManagedThreadId);   
  31.                     }   
  32.                 );   
  33.         }   
  34.   
  35.         Thread.Sleep(3000);   
  36.         sem.Release(3);   
  37.         Console.ReadLine();   
  38.     }   
  39.     static void Main(string[] args)   
  40.     {   
  41.        //Semaphores have no notion of ThreadID, any thread can call a release and it wil decrease the semaphore count   
  42.         RunNamedSemaphore();   
  43.         RunUnNamedSemaphore();   
  44.     }   
  45. }   

To see the RunNamedSemaphore() method in action,comment out RunUnNamedSemaphore() and run 2 isntances of the console app.

Monitor
The Monitor is one of the most commonly used synchronization primitives used in the .NET framwework, everyone in some way shape or form has used it knowingly or unknowingly using the "lock" statement which is the syntactic sugar provided by the compiler. A Monitor is used for controlling access to a critical section of code scoped to a single process by granting a lock on an object to the requesting thread.The class has no constructor and exposes it's functionality via static methods. A thread can request a lock by calling Monitor.Enter(object o) and give up the lock by calling Monitor.Exit(object o). Only private or internal objects should be used for locking on, failure to do so is an invitation to deadlocks and/or race conditions While a lock is held by a thread, all other threads requesting a lock are blocked. For each synchronised object the following information is maintained:

  • A reference to the thread that currently holds the lock.
  • A reference to a ready queue, which contains the threads that are ready to obtain the lock.
  • A reference to a waiting queue, which contains the threads that are waiting for notification of a change in the state of the locked object.

To ensure that the Monitor is released properly, wrap your code in a try block and place the Exit call in a finally block. The functionality provided by the Enter and Exit methods is identical to that provided by the C# lock statement, except that lock wraps the Enter method overload and the Exit method in a try…finally block to ensure that the Monitor is released properly. .NET 4.0 provides a TryEnter which tries to acquire a lock and atomically sets a value indicating whether the lock was acquired. A timeout paramter indicates how long to wait for the lock to become available. The benefit of using the static methods on Monitor as opposed to the lock keyword is that it provided a greater degree of control. For instance if a Thread that holds a lock needs to decides to yield control temporarily (such as when it needs to perform an I/O bound operation), it can do so by calling Wait() which puts an entry into the wait queue and allows threads that are ready to run and waiting on the lock (from the ready queue) a chance to run. The thread that gets the lock can complete it's work and call Pulse or PulseAll() to notify the threads in the wait queue so that they can attempt to get the lock back

Example

  1. class MonitorSample   
  2. {   
  3.     private static void RunMonitor()   
  4.     {           object o = new object();   
  5.         for (int i = 0; i < 3; i++)   
  6.         {   
  7.             ThreadPool.QueueUserWorkItem(_ =>   
  8.             {   
  9.                 try  
  10.                 {   
  11.                     Monitor.Enter(o);   
  12.                     Console.WriteLine("Thread {0} acquired lock...working", Thread.CurrentThread.ManagedThreadId);   
  13.                     Thread.Sleep(2000);   
  14.                     Console.WriteLine("Thread {0} performing some I/O operation so yielding the lock temporarily...", Thread.CurrentThread.ManagedThreadId);   
  15.                     Monitor.PulseAll(o);   
  16.                     Monitor.Wait(o);   
  17.                     Console.WriteLine("Thread {0} reacquired lock", Thread.CurrentThread.ManagedThreadId);   
  18.                 }   
  19.                 finally  
  20.                 {   
  21.                     Console.WriteLine("Thread {0} released lock", Thread.CurrentThread.ManagedThreadId);   
  22.                     Monitor.PulseAll(o);   
  23.                     Monitor.Exit(o);   
  24.                 }   
  25.             }   
  26.             );   
  27.         }   
  28.   
  29.         Console.ReadLine();   
  30.   
  31.     }   
  32.     static void Main(string[] args)   
  33.     {   
  34.         RunMonitor();   
  35.     }   
  36. }   

AutoResetEvent
AutoResetEvents are a signalling mechanism for obtaining exclusive access to a resource. Threads can call WaitOne() to wait for a signal to be set by a thread that currently has exclusive access and is ready to give it up. Setting the event allows a single thread from the set of blocked threads, exclusive access and automtically resets the signal. AutoResetEvents inherit from WaitHandle which is a wrapper around a native kernel object, so it does carry some overhead. It might appear that Monitor and AutoResetEvents are both a way to synchronize access to a shared resource so which do you pick and why? AutoResetEvent being a signalling mechanism is primarily used in producer/consumer scenarios where in a producer thread can signal (call Set()) to indicate that some data has been produced and written to a buffer that worker threads can consume. One of which gets to pick up and process the data, and signal to the producer agai n that it is ready for more data to be produced and written to the buffer, whereas a Monitor is used to control access to a critical section of code which if left unsynchronized would cause data corruption.

Example:

  1. class AutoResetEventSample   
  2. {   
  3.     private static void EnqueueWorkItems()   
  4.     {   
  5.         //scenario:used when a thread needs exclusive access to a resource.   
  6.         AutoResetEvent e = new AutoResetEvent(false);   
  7.         for (int i = 0; i < 5; i++)   
  8.         {   
  9.             int index = i;   
  10.             ThreadPool.QueueUserWorkItem(_ =>   
  11.             {   
  12.                 Console.WriteLine("Thread {0}: waiting for event...", Thread.CurrentThread.ManagedThreadId);   
  13.                 e.WaitOne();   
  14.                 Console.WriteLine("Thread {0}: event signalled", Thread.CurrentThread.ManagedThreadId);   
  15.             }   
  16.                 );   
  17.         }   
  18.         Thread.Sleep(2000);   
  19.         Console.WriteLine("Main thread setting event");   
  20.         e.Set();   
  21.        }   
  22.        static void Main(string[] args)   
  23.       {   
  24.           EnqueueWorkItems();   
  25.            Console.ReadLine();   
  26.       }   
  27.    }   

ManualResetEvent
A ManualReset event is similar to AutoResetEvent, the primary difference being that once it is signalled(by calling Set(), the event is not automatically reset. The closest analogy is that of a flood gate that that has been opened allowing all threads waiting on the event to be signalled, access. ManualResetEvents are typically used in fork/join scenarios where in a main thread has forked a bunch of operations for child threads to complete and the main thread Waits on a bunch of WaitHandles, each of which is associated with one child. When a child completes it sets the event. When all events are set the threads have joined and the Main thread can continue about doing it's business.

Example:

  1. class ManualResetEventSample   
  2. {   
  3.     private static void EnqueueWorkItems()   
  4.     {   
  5.         //scenario:communication concerns a task in which one thread must complete   
  6.         //before other threads can proceed.   
  7.         ManualResetEvent e = new ManualResetEvent(false);   
  8.         for (int i = 0; i < 5; i++)   
  9.         {   
  10.             ThreadPool.QueueUserWorkItem(_ =>   
  11.                 {   
  12.                     Thread.Sleep(2000);   
  13.                     Console.WriteLine("Thread {0} waiting for event...", Thread.CurrentThread.ManagedThreadId);   
  14.                     e.WaitOne();   
  15.                     Console.WriteLine("Thread {0} event signalled", Thread.CurrentThread.ManagedThreadId);   
  16.                 }   
  17.                 );   
  18.         }   
  19.         Thread.Sleep(2000);   
  20.         Console.WriteLine("Main thread setting event");   
  21.         e.Set();   
  22.         Thread.Sleep(1000);   
  23.         Console.WriteLine("Main thread re-setting event");   
  24.         e.Reset();   
  25.         Thread.Sleep(1000);   
  26.         Console.WriteLine("Main thread setting event again");   
  27.         e.Set();   
  28.        }   
  29.        static void Main(string[] args)   
  30.       {   
  31.           EnqueueWorkItems();   
  32.            Console.ReadLine();   
  33.       }   
  34.    }   

ReaderWriterLock
A ReaderWriterLock is used to synchronize access to a resource where reads are more frequent than writes. It allows for a single thread (writer) exclusive access to the resource or allows multiple threads (readers) concurrent access to the resource. It is much more performant than a Monitor which does not distinguish between readers and writers thus providing better throughput. Multiple readers alternate with single writers, so that neither readers nor writers are blocked for long periods. Most of the methods for acquiring reader or writer locks accept a timeout parameter so that threads do not deadlock in cases when a thread holding a reader lock requests a writer lock whereas another thread holding the writer lock requests a reader lock.

Example:

  1. class ReaderWriterLockSample   
  2. {   
  3.     private static void Read(ReaderWriterLock readerWriterLock)   
  4.     {   
  5.         Console.WriteLine("Thread {0} waiting for reader lock", Thread.CurrentThread.ManagedThreadId);   
  6.         readerWriterLock.AcquireReaderLock(8000);   
  7.         // AcquireReaderLock can be called multiple times by the same thread,    
  8.         //it bumps up the lock count for this thread   
  9.         if (readerWriterLock.IsReaderLockHeld)   
  10.         {   
  11.             Console.WriteLine("Thread {0} acquired reader lock", Thread.CurrentThread.ManagedThreadId);   
  12.             Console.WriteLine("Thread {0} reading", Thread.CurrentThread.ManagedThreadId);   
  13.             Thread.Sleep(1000);   
  14.             Console.WriteLine("Thread {0} releasing reader lock", Thread.CurrentThread.ManagedThreadId);   
  15.   
  16.             //reduces lock count to 0 immediately, useful if the same thread has acquired the lock multiple   
  17.             //times and increased the lock count. Call "ReleaseReaderLock" to decrement   
  18.             //the lock count by one.                   
  19.             readerWriterLock.ReleaseLock();   
  20.         }   
  21.     }   
  22.   
  23.     private static void Write(ReaderWriterLock readerWriterLock)   
  24.     {   
  25.         Console.WriteLine("Thread {0} waiting for writer lock", Thread.CurrentThread.ManagedThreadId);   
  26.         //blocks  if another thread holds a reader or writer lock.   
  27.         readerWriterLock.AcquireWriterLock(3000);   
  28.         if (readerWriterLock.IsWriterLockHeld)   
  29.         {   
  30.             Console.WriteLine("Thread {0} acquired writer lock", Thread.CurrentThread.ManagedThreadId);   
  31.             Console.WriteLine("Thread {0} writing", Thread.CurrentThread.ManagedThreadId);   
  32.             Thread.Sleep(1000);   
  33.             Console.WriteLine("Thread {0} releasing writer lock", Thread.CurrentThread.ManagedThreadId);   
  34.   
  35.             //reduces lock count to 0 immediately, useful if the same thread has acquired the lock multiple   
  36.             //times and increased the lock count. Call "ReleaseWriterLock" to decrement   
  37.             //the lock count by one.                   
  38.             readerWriterLock.ReleaseLock();   
  39.         }   
  40.     }   
  41.   
  42.     private static void ReadUpgradeToWrite(ReaderWriterLock readerWriterLock)   
  43.     {   
  44.         try  
  45.         {   
  46.             Console.WriteLine("Thread {0} waiting for reader lock", Thread.CurrentThread.ManagedThreadId);   
  47.             //blocks  if another thread holds a reader or writer lock.   
  48.             readerWriterLock.AcquireReaderLock(3000);   
  49.             if (readerWriterLock.IsReaderLockHeld)   
  50.             {   
  51.                 Console.WriteLine("Thread {0} acquired reader lock", Thread.CurrentThread.ManagedThreadId);   
  52.                 Console.WriteLine("Thread {0} reading", Thread.CurrentThread.ManagedThreadId);   
  53.                 Thread.Sleep(1000);   
  54.                 //release reader lock and request write lock,if it times out the exception   
  55.                 //will not be thrown until the thread reacquires the reader lock   
  56.                 Console.WriteLine("Thread {0} waiting to upgrade to writer lock...", Thread.CurrentThread.ManagedThreadId);   
  57.                 LockCookie cookie = readerWriterLock.UpgradeToWriterLock(2000);   
  58.                 try  
  59.                 {   
  60.                     Console.WriteLine("Thread {0} upgraded to writer lock...writing", Thread.CurrentThread.ManagedThreadId);   
  61.   
  62.                 }   
  63.                 finally  
  64.                 {   
  65.                     Console.WriteLine("Thread {0} releasing writer lock and downgraded to reader lock...", Thread.CurrentThread.ManagedThreadId);   
  66.                     //restores reader lock right away ,even if other threads are waiting to write(or read as I understand)   
  67.                     readerWriterLock.DowngradeFromWriterLock(ref cookie);   
  68.                 }   
  69.             }   
  70.         }   
  71.         finally  
  72.         {   
  73.             Console.WriteLine("Thread {0} releasing reacquired reader lock", Thread.CurrentThread.ManagedThreadId);   
  74.             readerWriterLock.ReleaseLock();   
  75.         }   
  76.     }   
  77.   
  78.     private void AnyWritersSince(ReaderWriterLock readerWriterLock)   
  79.     {   
  80.         Console.WriteLine("Thread {0} waiting for reader lock", Thread.CurrentThread.ManagedThreadId);   
  81.         readerWriterLock.AcquireReaderLock(3000);   
  82.         if (readerWriterLock.IsReaderLockHeld)   
  83.         {   
  84.             try  
  85.             {   
  86.                 Console.WriteLine("Thread {0} acquired reader lock", Thread.CurrentThread.ManagedThreadId);   
  87.                 Thread.Sleep(1000);   
  88.                 var seq = readerWriterLock.WriterSeqNum;   
  89.                 var cookie = readerWriterLock.ReleaseLock();   
  90.                 Console.WriteLine("Thread {0} released reader lock", Thread.CurrentThread.ManagedThreadId);   
  91.                 Thread.Sleep(1000);   
  92.                 readerWriterLock.RestoreLock(ref cookie);   
  93.                 Console.WriteLine("Thread {0} restored reader lock", Thread.CurrentThread.ManagedThreadId);   
  94.                 if (readerWriterLock.AnyWritersSince(seq))   
  95.                 {   
  96.                     Console.WriteLine("data invalid due to writer writing");   
  97.                 }   
  98.                 else  
  99.                 {   
  100.                     Console.WriteLine("data still valid, no writers have written");   
  101.                 }   
  102.             }   
  103.             finally  
  104.             {   
  105.                 Console.WriteLine("Thread {0} releasing restored reader lock", Thread.CurrentThread.ManagedThreadId);   
  106.                 readerWriterLock.ReleaseLock();   
  107.             }   
  108.         }   
  109.     }   
  110.   
  111.     private static void Run()   
  112.     {   
  113.         ReaderWriterLock readerWriterLock = new ReaderWriterLock();   
  114.         for (int i = 0; i < 5; i++)   
  115.         {   
  116.             if (i == 2)   
  117.             {   
  118.                 ThreadPool.QueueUserWorkItem(_ =>   
  119.                     {   
  120.                         Write(readerWriterLock);   
  121.                     }   
  122.                     );   
  123.             }   
  124.             else  
  125.             {   
  126.                 ThreadPool.QueueUserWorkItem(_ =>   
  127.                 {   
  128.                     Read(readerWriterLock);   
  129.                 }   
  130.                 );   
  131.             }   
  132.         }   
  133.        }   
  134.   
  135.        static void Main(string[] args)   
  136.        {   
  137.            Run();   
  138.            Console.ReadLine();   
  139.        }       
  140.   


.NET 4 introduced "Slim" verisions of some of the primitives above which are lightweight counterparts

SemaphoreSlim
SemaphoreSlim is similar to a Semaphore except that it cannot be used for cross process synchronization and does not use a windows kernel semaphore.

Example:

  1.   class SemaphoreSlimSample   
  2.   
  3. private static void RunSemaphoreSlim()   
  4. {   
  5.     SemaphoreSlim sem = new SemaphoreSlim(0, 3);   
  6.     Console.WriteLine("Main Thread holds onto all 3 available entries");   
  7.   
  8.     for (int i = 0; i < 5; i++)   
  9.     {   
  10.   
  11.   
  12.         ThreadPool.QueueUserWorkItem(_ =>   
  13.         {   
  14.             Console.WriteLine("Thread {0} waiting to acquire semaphore", Thread.CurrentThread.ManagedThreadId);   
  15.             if (sem.Wait(2000))   
  16.             {   
  17.                 Console.WriteLine("Thread {0} acquired semaphore", Thread.CurrentThread.ManagedThreadId);   
  18.                 Thread.Sleep(2000);   
  19.                 sem.Release();   
  20.                 Console.WriteLine("Thread {0} released semaphore", Thread.CurrentThread.ManagedThreadId);   
  21.             }   
  22.             else  
  23.             {   
  24.                 Console.WriteLine("Thread {0} timed out while waiting to acquire semaphore", Thread.CurrentThread.ManagedThreadId);   
  25.             }   
  26.         }   
  27.             );   
  28.     }   
  29.     Thread.Sleep(2000);   
  30.     sem.Release(3);   
  31.     Console.ReadLine();   
  32. }   
  33.   
  34.   static void Main(string[] args)   
  35.   {   
  36.        RunSemaphoreSlim();   
  37.   }    
  38.   

ManualResetEventSlim
ManualResetEventSlim is intended to be used when the wait time will be extremenly short. It resorts to spinning for the specified spin count before resorting to a kernel based wait operation. The Kernel object is not allocated unless needed making this more performant than a ManualResetEvent

Example:

  1.     class ManualResetEventSlimSample   
  2.     {   
  3.         private static void EnqueueWorkItems()   
  4.         {   
  5.             //scenario:communication concerns a task in which one thread must complete   
  6.             //before other threads can proceed.   
  7.             //Use when wait times are short, uses spinning before resorting to using a wait    
  8.             //handle, this is less expensive than waiting.Also provides a mechanism for   
  9.             //cancellation and timeouts.   
  10.             ManualResetEventSlim e = new ManualResetEventSlim(false);   
  11.             for (int i = 0; i < 5; i++)   
  12.             {   
  13.                 ThreadPool.QueueUserWorkItem(_ =>   
  14.                 {   
  15.                     Thread.Sleep(2000);   
  16.                     Console.WriteLine("Thread {0} waiting for event...", Thread.CurrentThread.ManagedThreadId);   
  17.                     e.Wait();   
  18.                     Console.WriteLine("Thread {0} event signalled", Thread.CurrentThread.ManagedThreadId);   
  19.                 }   
  20.                     );   
  21.             }   
  22.             Thread.Sleep(2000);   
  23.             Console.WriteLine("Main thread setting event");   
  24.             e.Set();   
  25.             Thread.Sleep(1000);   
  26.             Console.WriteLine("Main thread re-setting event");   
  27.             e.Reset();   
  28.             Thread.Sleep(1000);   
  29.             Console.WriteLine("Main thread setting event again");   
  30.             e.Set();   
  31.         }   
  32.   
  33.         static void Main(string[] args)   
  34. {   
  35.            EnqueueWorkItems();   
  36.             Console.ReadLine();   
  37. }   
  38.     }   

ReaderWriterLockSlim
ReaderWriterLockSlim is a more performant version of ReaderWriterLock. ReaderWriterLockSlim does not suffer from writer starvation which can can occur with ReaderWriterLock. By default, it does not allow recursion when requesting locks. It also avoids potential deadlocks that can occur with ReaderWriterLock such as when a thread is holding a read lock and wants to acquire a write lock but another thread is already waiting to acquire a write lock but it cannot until the read lock is released. To achieve this,the ReaderWriterLockSlim allows a thread to enter in one of 3 modes: - Read: Multiple threads can enter the lock in read mode as long as there is no thread currently holding a write lock or waiting to acquire a write lock; if there are any such threads,the threads waiting to enter in read mode are blocked. -Upgradeable: This mode is intended for cases where a thread usually performs reads and might occassionally perform writes. A thread that holds an upgradeable lock on a resource can read and request a promotion to write by calling (Enter/TryEnter)WriteLock. Only one thread can be in this mode at a time. A thread waiting to enter the lock in this mode will block if there is a thread currently holding a lock in write mode of waiting to enter write mode. If there are threads in read mode, the thread that is upgrading to write will block. While the thread is blocked, other threads trying to enter read mode are blocked. When all threads have exited from read mode, the blocked upgradeable thread enters write mode. If there are other threads waiting to enter write mode, they remain blocked, because the single thread that is in upgradeable mode prevents them from gaining exclusive access to the resource. When the thread in upgradeable mode exits write mode, if there are any threads waiting to enter write mode, they get a chance to go first before any threads that are waiting to enter in read mode. - Write: A thread can enter the lock in write mode if no threads hold the lock in any mode,in which case it will block. While a thread is holding a write lock, no threads can enter the lock in any mode.

Example:

  1. class ReaderWriterLockSlimSample   
  2. {   
  3.     private static void Read(ReaderWriterLockSlim readerWriterLock)   
  4.     {   
  5.         Console.WriteLine("Thread {0} waiting for reader lock", Thread.CurrentThread.ManagedThreadId);   
  6.         try  
  7.         {   
  8.             readerWriterLock.EnterReadLock();   
  9.             if (readerWriterLock.IsReadLockHeld)   
  10.             {   
  11.                 Console.WriteLine("Thread {0} acquired reader lock", Thread.CurrentThread.ManagedThreadId);   
  12.                 Console.WriteLine("Thread {0} reading", Thread.CurrentThread.ManagedThreadId);   
  13.                 Thread.Sleep(1000);   
  14.                 Console.WriteLine("Thread {0} releasing reader lock", Thread.CurrentThread.ManagedThreadId);   
  15.             }   
  16.         }   
  17.         finally  
  18.         {   
  19.                 readerWriterLock.ExitReadLock();   
  20.         }   
  21.     }   
  22.   
  23.     private static void ReadWithUpgrade(ReaderWriterLockSlim readerWriterLock)   
  24.     {   
  25.         Console.WriteLine("Thread {0} waiting for upgradeable read lock", Thread.CurrentThread.ManagedThreadId);   
  26.         //only 1 thread can enter upgradeable mode, threads in read mode cannot enter    
  27.         //upgradeable mode or write modedue to probability of deadlock.   
  28.         readerWriterLock.EnterUpgradeableReadLock();   
  29.         try  
  30.         {                  
  31.             if (readerWriterLock.IsUpgradeableReadLockHeld)   
  32.             {   
  33.                 Console.WriteLine("Thread {0} acquired upgradeable read lock", Thread.CurrentThread.ManagedThreadId);   
  34.                 Console.WriteLine("Thread {0} reading", Thread.CurrentThread.ManagedThreadId);   
  35.                 Thread.Sleep(1000);   
  36.                 Console.WriteLine("Thread {0} waiting to acquire write lock", Thread.CurrentThread.ManagedThreadId);   
  37.                 readerWriterLock.EnterWriteLock();   
  38.                 try  
  39.                 {   
  40.                     Console.WriteLine("Thread {0} to acquired write lock", Thread.CurrentThread.ManagedThreadId);   
  41.                 }   
  42.                 finally  
  43.                 {   
  44.                     Console.WriteLine("Thread {0} releasing write lock", Thread.CurrentThread.ManagedThreadId);   
  45.                     readerWriterLock.ExitWriteLock();   
  46.                 }   
  47.             }   
  48.         }   
  49.         finally  
  50.         {   
  51.             Console.WriteLine("Thread {0} releasing upgradeable read lock", Thread.CurrentThread.ManagedThreadId);   
  52.             //downgrade to read mode before leaving upgradeable mode.   
  53.             readerWriterLock.EnterReadLock();   
  54.             readerWriterLock.ExitUpgradeableReadLock();   
  55.             Console.WriteLine("Thread {0} IsReadLockHeld:{1}", Thread.CurrentThread.ManagedThreadId,readerWriterLock.IsReadLockHeld);   
  56.             Console.WriteLine("Thread {0} releasing read lock", Thread.CurrentThread.ManagedThreadId);   
  57.             readerWriterLock.ExitReadLock();   
  58.         }   
  59.     }   
  60.   
  61.     private static void Write(ReaderWriterLockSlim readerWriterLock)   
  62.     {   
  63.         Console.WriteLine("Thread {0} waiting for writer lock", Thread.CurrentThread.ManagedThreadId);   
  64.         try  
  65.         {   
  66.             readerWriterLock.EnterWriteLock();   
  67.             if (readerWriterLock.IsWriteLockHeld)   
  68.             {   
  69.                 Console.WriteLine("Thread {0} acquired writer lock", Thread.CurrentThread.ManagedThreadId);   
  70.                 Console.WriteLine("Thread {0} writing", Thread.CurrentThread.ManagedThreadId);   
  71.                 Thread.Sleep(1000);   
  72.                 Console.WriteLine("Thread {0} releasing readewriterr lock", Thread.CurrentThread.ManagedThreadId);   
  73.             }   
  74.         }   
  75.         finally  
  76.         {   
  77.             readerWriterLock.ExitWriteLock();   
  78.         }   
  79.     }   
  80.   
  81.     private static void Run()   
  82.     {   
  83.         ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim();   
  84.         for (int i = 0; i < 3; i++)   
  85.         {   
  86.             if (i == 1)   
  87.             {   
  88.                 ThreadPool.QueueUserWorkItem(_ =>   
  89.                 {   
  90.                     ReadWithUpgrade(readerWriterLock);   
  91.                 }   
  92.                     );   
  93.             }   
  94.             else  
  95.             {   
  96.                 ThreadPool.QueueUserWorkItem(_ =>   
  97.                 {   
  98.                     Read(readerWriterLock);   
  99.                 }   
  100.                 );   
  101.             }   
  102.         }   
  103.        }               
  104.        static void Main(string[] args)   
  105.        {   
  106.            Run();   
  107.            Console.ReadLine();   
  108.        }   
  109.    }   

AutoResetEvent does not have a "Slim" counterpart since AutoResetEvent is typically used when a thread need exclusive access to a resource for a period of time and wait times are not expected to be short. ManualResetEventSlim, is intended when you know, in advance that wait time will be very short and it uses spinning for a a period of time before waiting, hence there is no "Slim" counterpart. Unless you are on an older version of the framework there is no good reason to choose ReaderWriterLock over ReaderWriterLockSlim.

As the adage goes, "with great power comes great responsibility", use the above with care, writing thread synchronization code is easy, writing bug free and performant thread synchronization code is non trivial. Use libraries like TPL which have done the hard work and encapsulated common patterns of parallelism and asynchrony and exposed them as tasks, but keep the above in mind and konw that they are available for those niche cases when you need fine grained control. This post turned out longer than I anticipated but it serves a one stop shop (at least for me) to come to at those times when I need a quick refresher.


Print | posted @ Sunday, July 15, 2012 6:58 PM

Comments on this entry:

Gravatar # re: Mutexes, Semaphores, Monitors, ReaderWriterLocks, AutoResetEvents & ManualResets oh my!
by schglurps at 7/16/2012 3:12 AM

Great article, thank you !
Gravatar # re: Mutexes, Semaphores, Monitors, ReaderWriterLocks, AutoResetEvents & ManualResets oh my!
by Kannan at 7/16/2012 7:22 AM

Nicely written ! Thanks for sharing.
Gravatar # re: Mutexes, Semaphores, Monitors, ReaderWriterLocks, AutoResetEvents & ManualResets oh my!
by PPetrov at 7/17/2012 12:55 AM

Great article! The only class review that I miss is CountdownEvent(http://msdn.microsoft.com/en-us/library/system.threading.countdownevent.aspx) which I use very often in the scenario you described for ManualResetEvent. CountdownEvent(16) is much better than an array of 16 ManualResetEvents :) Probably you can also mention WaitAny/WaitAll methods and their use. One more time - Great article!
Gravatar # re: Mutexes, Semaphores, Monitors, ReaderWriterLocks, AutoResetEvents & ManualResets oh my!
by Abhishek at 8/28/2012 2:28 AM

Very Good in very simple language
Gravatar # re: Mutexes, Semaphores, Monitors, ReaderWriterLocks, AutoResetEvents & ManualResets oh my!
by Roopa at 10/29/2012 4:30 AM

Good one, thank you
Post A Comment
Title:
Name:
Email:
Comment:
Verification: