• This MSDN document gives a good survey on various synchronization primitives in .NET. This article will follow how it categorizes the synchronization primitives.
  • Threading in C# is a very good high level overview on synchronization. It has a slightly different way of categorizing sync primitives. It also offers a few ones that're not mentioned in the other doc.
  • This paper describes some .NET 4.0 new primitives and provides insight into their implementation and performance consideration.

Synchronization is mostly about the mechanisms provided by the language to perform waiting (aka blocking). Those mechanisms vary depending on how the waiting is achieved, and by what criteria the waiting should finish (aka released, or unblocked).

Exclusive Locks

lock, Monitor, Mutex are exclusive locks. lock is the most convenient to use. Monitor provides richer options when waiting on the lock, e.g., timeout, etc. Mutex provides inter-process locking but is more expensive to use.

lock vs Mutex:

[...] Of the two (lock and Mutex), the lock construct is faster and more convenient. Mutex, though, has a niche in that its lock can span applications in different processes on the computer.

Reader Writer Lock

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.

From MSDN:

ReaderWriterLockSlim is similar to ReaderWriterLock, but it has simplified rules for recursion and for upgrading and downgrading lock state. ReaderWriterLockSlimavoids many cases of potential deadlock. In addition, the performance of ReaderWriterLockSlim is significantly better than ReaderWriterLock. ReaderWriterLockSlim is recommended for all new development.

Semaphore

Semaphore is more general than mutually exclusive locks by allowing a specified number of threads to access a resource. Like Mutex, it can be used across process. SemaphoreSlim is its local and more efficient version.

Wait Handles

Conceptual overview: EventWaitHandle, AutoResetEvent, CountdownEvent, ManualResetEvent.

                          WaitHandle (abstract)
                             |
                 +-----------+----------------+
                 |                 |          |
                 |                 |          |
                 v                 v          v
          EventWaitHandle      Semaphore    Mutex
          +           +
          |           |
          |           |
          v           v
 AutoResetEvent   ManualResetEvent

Wait handles can be waited on. But what's more interesting is the events. An event is a value with two states - "signaled" and "not-signaled". When waiting on it, a thread is only unblocked once the event is "signaled". An event can be signaled by calling its .Set() method.

  • AutoResetEvent is auto reset to "not-signaled" state when one waiting thread is unblocked, therefore continue to blocking the rest of the waiting threads. Effectively, it lets only one thread to "pass through" at a time.
  • ManualResetEvent does not auto reset so it lets all waiting threads to pass through once signaled.

There are lightweight counterparts of the above mentioned primitives that're faster and doesn't work across process:

  • System.Threading.SemaphoreSlim is a lightweight version of System.Threading.Semaphore.
  • System.Threading.ManualResetEventSlim is a lightweight version of System.Threading.ManualResetEvent.
  • System.Threading.CountdownEvent represents an event that becomes signaled when its count is zero.
  • System.Threading.Barrier enables multiple threads to synchronize with one another without requiring control by a master thread. A barrier prevents each thread from continuing until all threads have reached a specified point.

Spin based primitives

SpinWait can be used to wait for a condition Func<bool> to meet. It uses a good combination of spinning (initially) and blocking (yielding the thread, after an excessive time spinning).

SpinLock is a lock based on spinning. It doesn't turn into blocking so care must be taken when holding such a lock. Only for advanced and very performance critical uses.

Interlocked Operations

Lastly, Interlocked is not blocking at all. From MSDN:

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.

TaskCompletionSource

This can be used to synchronize tasks the way events are used for threads. Tasks and async/await is asynchronous computing on a higher level than threads. And in addition to the built in constructs that one can await on, TaskCompletionSource<T> can be used to signal and synchronous anything among tasks.

Thread Affinity and Its Issues When Used with Tasks

Some sync mechanisms described above has thread affinity and it has issues when used with Tasks. For example, tasks do not work well with monitors. As monitors have thread affinity, a task, resumed onto a different thread than its original thread, can release the wrong monitor; vise versa, a task resumed to a thread can aquire the monitor while it has already been aquired by a previously blocked task.

So it's important to think about thread affinity when picking a sync mechanism to work with tasks. As a summary:

These are thread affine:

  • Monitors (and locks)
  • Reader-writer locks
  • Mutex

These does not have thread affinity:

  • Semaphore (and SemaphoreSlim)
  • Event wait handles
  • TaskCompletionSource<T>