- 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).
Mutex are exclusive locks.
lock is the most convenient
Monitor provides richer options when waiting on the lock, e.g.,
Mutex provides inter-process locking but is more expensive to
[...] 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.
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 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
SemaphoreSlim is its local and more efficient version.
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
AutoResetEventis 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.
ManualResetEventdoes 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.SemaphoreSlimis a lightweight version of System.Threading.Semaphore.
System.Threading.ManualResetEventSlimis a lightweight version of System.Threading.ManualResetEvent.
System.Threading.CountdownEventrepresents an event that becomes signaled when its count is zero.
System.Threading.Barrierenables 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
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.
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
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
These does not have thread affinity:
- Semaphore (and SemaphoreSlim)
- Event wait handles