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:
- class MutexSample
- {
- private static void RunMutex()
- {
- Mutex mutex = new Mutex(true);
- Console.WriteLine("Main thread holds mutex");
- ThreadPool.QueueUserWorkItem(_ =>
- {
- Console.WriteLine("Thread {0} waiting to acquire mutex", Thread.CurrentThread.ManagedThreadId);
- mutex.WaitOne();
- Console.WriteLine("Thread {0} acquired mutex", Thread.CurrentThread.ManagedThreadId);
- Thread.Sleep(2000);
- Console.WriteLine("Thread {0} released mutex", Thread.CurrentThread.ManagedThreadId);
- mutex.ReleaseMutex();
- }
- );
- Thread.Sleep(5000);
- mutex.ReleaseMutex();
- Console.WriteLine("Main thread released mutex");
- Console.ReadLine();
- }
-
- private static void RunNamedMutex()
- {
- Mutex mutex = new Mutex(false, "MYNAMEDMUTEX");
- Console.WriteLine("Main thread waiting to acquire mutex");
- mutex.WaitOne();
- Console.WriteLine("Main thread holds mutex");
-
- Thread.Sleep(8000);
- mutex.ReleaseMutex();
- Console.WriteLine("Main thread released mutex");
- Console.ReadLine();
- }
- static void Main(string[] args)
- {
-
-
- RunMutex();
- RunNamedMutex();
- }
- }
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:
- class SemaphoreSample
- {
- private static void RunNamedSemaphore()
- {
- Semaphore sem = new Semaphore(1, 1, "MYNAMEDSEMAPHORE");
- Console.WriteLine("Main Thread holds onto all 3 available entries");
- sem.WaitOne();
- Console.WriteLine("Acquired semaphore");
- Thread.Sleep(8000);
- sem.Release();
- Console.WriteLine("Released semaphore");
- Console.ReadLine();
- }
-
- private static void RunUnNamedSemaphore()
- {
- Semaphore sem = new Semaphore(0, 3);
- Console.WriteLine("Main Thread holds onto all 3 available entries");
- for (int i = 0; i < 5; i++)
- {
-
-
- ThreadPool.QueueUserWorkItem(_ =>
- {
- Console.WriteLine("Thread {0} waiting to acquire semaphore", Thread.CurrentThread.ManagedThreadId);
- sem.WaitOne();
- Console.WriteLine("Thread {0} acquired semaphore", Thread.CurrentThread.ManagedThreadId);
- Thread.Sleep(2000);
- sem.Release();
- Console.WriteLine("Thread {0} releases semaphore", Thread.CurrentThread.ManagedThreadId);
- }
- );
- }
-
- Thread.Sleep(3000);
- sem.Release(3);
- Console.ReadLine();
- }
- static void Main(string[] args)
- {
-
- RunNamedSemaphore();
- RunUnNamedSemaphore();
- }
- }
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
- class MonitorSample
- {
- private static void RunMonitor()
- { object o = new object();
- for (int i = 0; i < 3; i++)
- {
- ThreadPool.QueueUserWorkItem(_ =>
- {
- try
- {
- Monitor.Enter(o);
- Console.WriteLine("Thread {0} acquired lock...working", Thread.CurrentThread.ManagedThreadId);
- Thread.Sleep(2000);
- Console.WriteLine("Thread {0} performing some I/O operation so yielding the lock temporarily...", Thread.CurrentThread.ManagedThreadId);
- Monitor.PulseAll(o);
- Monitor.Wait(o);
- Console.WriteLine("Thread {0} reacquired lock", Thread.CurrentThread.ManagedThreadId);
- }
- finally
- {
- Console.WriteLine("Thread {0} released lock", Thread.CurrentThread.ManagedThreadId);
- Monitor.PulseAll(o);
- Monitor.Exit(o);
- }
- }
- );
- }
-
- Console.ReadLine();
-
- }
- static void Main(string[] args)
- {
- RunMonitor();
- }
- }
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:
- class AutoResetEventSample
- {
- private static void EnqueueWorkItems()
- {
-
- AutoResetEvent e = new AutoResetEvent(false);
- for (int i = 0; i < 5; i++)
- {
- int index = i;
- ThreadPool.QueueUserWorkItem(_ =>
- {
- Console.WriteLine("Thread {0}: waiting for event...", Thread.CurrentThread.ManagedThreadId);
- e.WaitOne();
- Console.WriteLine("Thread {0}: event signalled", Thread.CurrentThread.ManagedThreadId);
- }
- );
- }
- Thread.Sleep(2000);
- Console.WriteLine("Main thread setting event");
- e.Set();
- }
- static void Main(string[] args)
- {
- EnqueueWorkItems();
- Console.ReadLine();
- }
- }
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:
- class ManualResetEventSample
- {
- private static void EnqueueWorkItems()
- {
-
-
- ManualResetEvent e = new ManualResetEvent(false);
- for (int i = 0; i < 5; i++)
- {
- ThreadPool.QueueUserWorkItem(_ =>
- {
- Thread.Sleep(2000);
- Console.WriteLine("Thread {0} waiting for event...", Thread.CurrentThread.ManagedThreadId);
- e.WaitOne();
- Console.WriteLine("Thread {0} event signalled", Thread.CurrentThread.ManagedThreadId);
- }
- );
- }
- Thread.Sleep(2000);
- Console.WriteLine("Main thread setting event");
- e.Set();
- Thread.Sleep(1000);
- Console.WriteLine("Main thread re-setting event");
- e.Reset();
- Thread.Sleep(1000);
- Console.WriteLine("Main thread setting event again");
- e.Set();
- }
- static void Main(string[] args)
- {
- EnqueueWorkItems();
- Console.ReadLine();
- }
- }
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:
- class ReaderWriterLockSample
- {
- private static void Read(ReaderWriterLock readerWriterLock)
- {
- Console.WriteLine("Thread {0} waiting for reader lock", Thread.CurrentThread.ManagedThreadId);
- readerWriterLock.AcquireReaderLock(8000);
-
-
- if (readerWriterLock.IsReaderLockHeld)
- {
- Console.WriteLine("Thread {0} acquired reader lock", Thread.CurrentThread.ManagedThreadId);
- Console.WriteLine("Thread {0} reading", Thread.CurrentThread.ManagedThreadId);
- Thread.Sleep(1000);
- Console.WriteLine("Thread {0} releasing reader lock", Thread.CurrentThread.ManagedThreadId);
-
-
-
-
- readerWriterLock.ReleaseLock();
- }
- }
-
- private static void Write(ReaderWriterLock readerWriterLock)
- {
- Console.WriteLine("Thread {0} waiting for writer lock", Thread.CurrentThread.ManagedThreadId);
-
- readerWriterLock.AcquireWriterLock(3000);
- if (readerWriterLock.IsWriterLockHeld)
- {
- Console.WriteLine("Thread {0} acquired writer lock", Thread.CurrentThread.ManagedThreadId);
- Console.WriteLine("Thread {0} writing", Thread.CurrentThread.ManagedThreadId);
- Thread.Sleep(1000);
- Console.WriteLine("Thread {0} releasing writer lock", Thread.CurrentThread.ManagedThreadId);
-
-
-
-
- readerWriterLock.ReleaseLock();
- }
- }
-
- private static void ReadUpgradeToWrite(ReaderWriterLock readerWriterLock)
- {
- try
- {
- Console.WriteLine("Thread {0} waiting for reader lock", Thread.CurrentThread.ManagedThreadId);
-
- readerWriterLock.AcquireReaderLock(3000);
- if (readerWriterLock.IsReaderLockHeld)
- {
- Console.WriteLine("Thread {0} acquired reader lock", Thread.CurrentThread.ManagedThreadId);
- Console.WriteLine("Thread {0} reading", Thread.CurrentThread.ManagedThreadId);
- Thread.Sleep(1000);
-
-
- Console.WriteLine("Thread {0} waiting to upgrade to writer lock...", Thread.CurrentThread.ManagedThreadId);
- LockCookie cookie = readerWriterLock.UpgradeToWriterLock(2000);
- try
- {
- Console.WriteLine("Thread {0} upgraded to writer lock...writing", Thread.CurrentThread.ManagedThreadId);
-
- }
- finally
- {
- Console.WriteLine("Thread {0} releasing writer lock and downgraded to reader lock...", Thread.CurrentThread.ManagedThreadId);
-
- readerWriterLock.DowngradeFromWriterLock(ref cookie);
- }
- }
- }
- finally
- {
- Console.WriteLine("Thread {0} releasing reacquired reader lock", Thread.CurrentThread.ManagedThreadId);
- readerWriterLock.ReleaseLock();
- }
- }
-
- private void AnyWritersSince(ReaderWriterLock readerWriterLock)
- {
- Console.WriteLine("Thread {0} waiting for reader lock", Thread.CurrentThread.ManagedThreadId);
- readerWriterLock.AcquireReaderLock(3000);
- if (readerWriterLock.IsReaderLockHeld)
- {
- try
- {
- Console.WriteLine("Thread {0} acquired reader lock", Thread.CurrentThread.ManagedThreadId);
- Thread.Sleep(1000);
- var seq = readerWriterLock.WriterSeqNum;
- var cookie = readerWriterLock.ReleaseLock();
- Console.WriteLine("Thread {0} released reader lock", Thread.CurrentThread.ManagedThreadId);
- Thread.Sleep(1000);
- readerWriterLock.RestoreLock(ref cookie);
- Console.WriteLine("Thread {0} restored reader lock", Thread.CurrentThread.ManagedThreadId);
- if (readerWriterLock.AnyWritersSince(seq))
- {
- Console.WriteLine("data invalid due to writer writing");
- }
- else
- {
- Console.WriteLine("data still valid, no writers have written");
- }
- }
- finally
- {
- Console.WriteLine("Thread {0} releasing restored reader lock", Thread.CurrentThread.ManagedThreadId);
- readerWriterLock.ReleaseLock();
- }
- }
- }
-
- private static void Run()
- {
- ReaderWriterLock readerWriterLock = new ReaderWriterLock();
- for (int i = 0; i < 5; i++)
- {
- if (i == 2)
- {
- ThreadPool.QueueUserWorkItem(_ =>
- {
- Write(readerWriterLock);
- }
- );
- }
- else
- {
- ThreadPool.QueueUserWorkItem(_ =>
- {
- Read(readerWriterLock);
- }
- );
- }
- }
- }
-
- static void Main(string[] args)
- {
- Run();
- Console.ReadLine();
- }
-
.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:
- class SemaphoreSlimSample
-
- private static void RunSemaphoreSlim()
- {
- SemaphoreSlim sem = new SemaphoreSlim(0, 3);
- Console.WriteLine("Main Thread holds onto all 3 available entries");
-
- for (int i = 0; i < 5; i++)
- {
-
-
- ThreadPool.QueueUserWorkItem(_ =>
- {
- Console.WriteLine("Thread {0} waiting to acquire semaphore", Thread.CurrentThread.ManagedThreadId);
- if (sem.Wait(2000))
- {
- Console.WriteLine("Thread {0} acquired semaphore", Thread.CurrentThread.ManagedThreadId);
- Thread.Sleep(2000);
- sem.Release();
- Console.WriteLine("Thread {0} released semaphore", Thread.CurrentThread.ManagedThreadId);
- }
- else
- {
- Console.WriteLine("Thread {0} timed out while waiting to acquire semaphore", Thread.CurrentThread.ManagedThreadId);
- }
- }
- );
- }
- Thread.Sleep(2000);
- sem.Release(3);
- Console.ReadLine();
- }
-
- static void Main(string[] args)
- {
- RunSemaphoreSlim();
- }
-
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:
- class ManualResetEventSlimSample
- {
- private static void EnqueueWorkItems()
- {
-
-
-
-
-
- ManualResetEventSlim e = new ManualResetEventSlim(false);
- for (int i = 0; i < 5; i++)
- {
- ThreadPool.QueueUserWorkItem(_ =>
- {
- Thread.Sleep(2000);
- Console.WriteLine("Thread {0} waiting for event...", Thread.CurrentThread.ManagedThreadId);
- e.Wait();
- Console.WriteLine("Thread {0} event signalled", Thread.CurrentThread.ManagedThreadId);
- }
- );
- }
- Thread.Sleep(2000);
- Console.WriteLine("Main thread setting event");
- e.Set();
- Thread.Sleep(1000);
- Console.WriteLine("Main thread re-setting event");
- e.Reset();
- Thread.Sleep(1000);
- Console.WriteLine("Main thread setting event again");
- e.Set();
- }
-
- static void Main(string[] args)
- {
- EnqueueWorkItems();
- Console.ReadLine();
- }
- }
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:
- class ReaderWriterLockSlimSample
- {
- private static void Read(ReaderWriterLockSlim readerWriterLock)
- {
- Console.WriteLine("Thread {0} waiting for reader lock", Thread.CurrentThread.ManagedThreadId);
- try
- {
- readerWriterLock.EnterReadLock();
- if (readerWriterLock.IsReadLockHeld)
- {
- Console.WriteLine("Thread {0} acquired reader lock", Thread.CurrentThread.ManagedThreadId);
- Console.WriteLine("Thread {0} reading", Thread.CurrentThread.ManagedThreadId);
- Thread.Sleep(1000);
- Console.WriteLine("Thread {0} releasing reader lock", Thread.CurrentThread.ManagedThreadId);
- }
- }
- finally
- {
- readerWriterLock.ExitReadLock();
- }
- }
-
- private static void ReadWithUpgrade(ReaderWriterLockSlim readerWriterLock)
- {
- Console.WriteLine("Thread {0} waiting for upgradeable read lock", Thread.CurrentThread.ManagedThreadId);
-
-
- readerWriterLock.EnterUpgradeableReadLock();
- try
- {
- if (readerWriterLock.IsUpgradeableReadLockHeld)
- {
- Console.WriteLine("Thread {0} acquired upgradeable read lock", Thread.CurrentThread.ManagedThreadId);
- Console.WriteLine("Thread {0} reading", Thread.CurrentThread.ManagedThreadId);
- Thread.Sleep(1000);
- Console.WriteLine("Thread {0} waiting to acquire write lock", Thread.CurrentThread.ManagedThreadId);
- readerWriterLock.EnterWriteLock();
- try
- {
- Console.WriteLine("Thread {0} to acquired write lock", Thread.CurrentThread.ManagedThreadId);
- }
- finally
- {
- Console.WriteLine("Thread {0} releasing write lock", Thread.CurrentThread.ManagedThreadId);
- readerWriterLock.ExitWriteLock();
- }
- }
- }
- finally
- {
- Console.WriteLine("Thread {0} releasing upgradeable read lock", Thread.CurrentThread.ManagedThreadId);
-
- readerWriterLock.EnterReadLock();
- readerWriterLock.ExitUpgradeableReadLock();
- Console.WriteLine("Thread {0} IsReadLockHeld:{1}", Thread.CurrentThread.ManagedThreadId,readerWriterLock.IsReadLockHeld);
- Console.WriteLine("Thread {0} releasing read lock", Thread.CurrentThread.ManagedThreadId);
- readerWriterLock.ExitReadLock();
- }
- }
-
- private static void Write(ReaderWriterLockSlim readerWriterLock)
- {
- Console.WriteLine("Thread {0} waiting for writer lock", Thread.CurrentThread.ManagedThreadId);
- try
- {
- readerWriterLock.EnterWriteLock();
- if (readerWriterLock.IsWriteLockHeld)
- {
- Console.WriteLine("Thread {0} acquired writer lock", Thread.CurrentThread.ManagedThreadId);
- Console.WriteLine("Thread {0} writing", Thread.CurrentThread.ManagedThreadId);
- Thread.Sleep(1000);
- Console.WriteLine("Thread {0} releasing readewriterr lock", Thread.CurrentThread.ManagedThreadId);
- }
- }
- finally
- {
- readerWriterLock.ExitWriteLock();
- }
- }
-
- private static void Run()
- {
- ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim();
- for (int i = 0; i < 3; i++)
- {
- if (i == 1)
- {
- ThreadPool.QueueUserWorkItem(_ =>
- {
- ReadWithUpgrade(readerWriterLock);
- }
- );
- }
- else
- {
- ThreadPool.QueueUserWorkItem(_ =>
- {
- Read(readerWriterLock);
- }
- );
- }
- }
- }
- static void Main(string[] args)
- {
- Run();
- Console.ReadLine();
- }
- }
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.