[C#] Concurrency cheat sheet
The .NET Framework provides a ridiculous number of solutions to deal with concurrency. You probably know the lock
statement or the ManualResetEvent
class, but you’ll see that there are many more options.
Life is always easier when you choose the appropriate tool, so you’d better know what’s available. This article gathers all the information in one place.
Table of content:
ArrayList.Synchronized
methodAutoResetEvent
classBarrier
classBlockingCollection
classCancellationToken
structConcurrentBag
classConcurrentDictionary
classConcurrentQueue
classConcurrentStack
classCountdownEvent
classEventWaitHandle
classInterlocked
operationslock
statementManualResetEvent
classManualResetEventSlim
classMethodImplOptions.Synchronized
attributeMonitor.Enter
and.Exit
methodsMonitor.Pulse
methodMonitor.PulseAll
methodMutex
classReaderWriterLock
classReaderWriterLockSlim
classSemaphore
classSemaphoreSlim
classSpinLock
classSpinWait
classThread.MemoryBarrier
method (andInterlocked.MemoryBarrier
)Thread.VolatileRead
and.VolatileWrite
methodsvolatile
keywordVolatile.Read
andWrite
methods
1. ArrayList.Synchronized
For those who still use ArrayList
, there is a simple way to make it thread safe:
array = ArrayList.Synchronized(new ArrayList());
ArrayList.Synchronized()
returns a thread-safe wrapper of the given array. Most operation, such as array.Add()
, array[i]
or array.Count
, are thread safe.
Unfortunately, enumerating the ArrayList
is not thread-safe. The recommended pattern is:
lock(array.SyncRoot)
{
foreach (object item in array)
{
// ...
}
}
Pros | Easy to use |
---|---|
Cons | Only works for ArrayList |
Since | .NET Framework 1.0 |
Alt. | ConcurrentBag, ConcurrentQueue, ConcurrentStack |
Links | MSDN: ArrayList.Synchronized Method (ArrayList) |
2. AutoResetEvent
When signaled, a AutoResetEvent
releases exactly one waiting thread. It’s commonly use to perform exclusive access. This is different from a ManualResetEvent
which releases all waiting threads.
readonly AutoResetEvent autoResetEvent = new AutoResetEvent(true);
autoResetEvent.WaitOne();
// ...insert code here...
autoResetEvent.Set();
In this particular example, and AutoResetEvent
behaves like a Semaphore
with max set to 1.
Pros | Resumes only one waiting thread |
---|---|
Cons | Doesn't support named events |
Since | .NET Framework 1.0 |
Alt. | EventWaitHandle, lock, Monitor, Mutex, Semaphore |
Links |
MSDN: AutoResetEvent Class MSDN: EventWaitHandle Class StackOverflow: ManualResetEvent vs AutoResetEvent StackOverflow: AutoResetEvent vs Semaphore |
3. Barrier
Barrier
class helps synchronizing parallel operations when an algorithm is composed of several phases. Threads will wait for each others so that they are always working on the same phase.
while (someCondition)
{
// ...insert code here...
barrier.SignalAndWait();
}
Pros | Very useful for parallel processing |
---|---|
Cons | Only for parallel algorithm with multiple phases |
Since | .NET Framework 4 |
Alt. | CountdownEvent, Task.WaitAll(), Thread.Join(), Semaphore |
Links |
MSDN: Barrier Class MSDN: Synchronize Concurrent Operations with a Barrier Wikipedia: Barrier (computer science) |
4. BlockingCollection
ConcurrentBag
, ConcurrentQueue
and ConcurrentStack
allows extracting values from the collection in a non-blocking manner. But what if you want to wait until the value is available?
BlockingCollection
is a wrapper that adds this feature: when you call Take()
, it will block until a value is available.
BlockingCollection
also comes with another feature: you can decide to limit the size of the collection. Attempts to add values to a collection that has reached the maximum size, will block the thread until room is available.
If you don’t specify the underlying collection, a ConcurrentQueue
is used.
collection = new BlockingCollection<T>();
collection = new BlockingCollection<T>(new ConcurrentBag<T>());
collection = new BlockingCollection<T>(new ConcurrentQueue<T>());
collection = new BlockingCollection<T>(new ConcurrentStack<T>());
collection.Add(value);
value = collection.Take();
Pros | Works with various collections |
---|---|
Cons | None |
Since | .NET Framework 4 |
Alt. | Semaphore, CountdownEvent |
Links | MSDN: BlockingCollection<T> Class |
5. CancellationToken
CancellationToken
is the preferred way to cancel an operation.
The master thread triggers the cancellation through a CancellationTokenSource
.
The slave thread must periodically poll the state of the CancellationToken
.
cancellationTokenSource = new CancellationTokenSource();
cancellationToken = cancellationTokenSource.Token;
// Master thread
cancellationTokenSource.Cancel();
// Slave thread
while (!cancellationToken.IsCancellationRequested)
{
// ... insert code here...
}
// Slave thread (other way to use it)
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
// ... insert code here...
}
Pros | Clear purpose: cancel |
---|---|
Cons | None |
Since | .NET Framework 4 |
Alt. | ManualResetEvent |
Links |
MSDN: CancellationToken Structure MSDN: CancellationTokenSource Class MSDN: Cancellation Tokens |
6. ConcurrentBag
ConcurrentBag
is a thread-safe unordered collection.
bag = new ConcurrentBag<T>();
bag.Add(value);
bag.TryPeek(out value);
bag.TryTake(out value);
Pros | Non blocking read access |
---|---|
Cons | Order is not preserved |
Since | .NET Framework 4 |
Alt. | ArrayList.Synchronized, ConcurrentQueue, ConcurrentStack |
Links | MSDN: ConcurrentBag<T> Class |
7. ConcurrentDictionary
ConcurrentDictionary
is a thread-safe Dictionary
. Even though it explicitly implements IDictionary
, its public API is quite different from a classic Dictionary
.
dict = new ConcurrentDictionary<TKey,TValue>();
dict.TryAdd(key, value);
dict.TryGetValue(key, out value);
dict.TryRemove(key, out value);
dict.AddOrUpdate(key, value, (k, v) => value);
dict.AddOrUpdate(key, k => value, (k, v) => value);
dict.GetOrAdd(key, k => value);
Pros | Non blocking read access |
---|---|
Cons | Not as simple as you expect |
Since | .NET Framework 4 |
Alt. | lock, Monitor |
Links | MSDN: ConcurrentDictionary<TKey, TValue> Class |
8. ConcurrentQueue
ConcurrentQueue
is a thread-safe version of Queue
.
queue = new ConcurrentQueue<T>();
queue.Enqueue(value);
queue.TryPeek(out value);
queue.TryDequeue(out value);
Pros | Non blocking read access |
---|---|
Cons | Interface differs from Queue |
Since | .NET Framework 4 |
Alt. | ConcurrentBag, ConcurrentStack, lock, Monitor |
Links | MSDN: ConcurrentQueue<T> Class |
9. ConcurrentStack
ConcurrentStack
is a thread-safe version of Stack
.
stack = new ConcurrentStack<T>();
stack.Push(value);
stack.TryPeek(out value);
stack.TryPop(out value);
Pros | Non blocking read access |
---|---|
Cons | Interface differs from Stack |
Since | .NET Framework 4 |
Alt. | ConcurrentBag, ConcurrentQueue, lock, Monitor |
Links | MSDN: ConcurrentStack<T> Class |
10. CountdownEvent
CountdownEvent
is like a reverse semaphore: it get signaled when the count reaches zero. For instance, it can be used to wait several thread to complete some operation.
readonly CountdownEvent cde = new CountdownEvent(0);
“Main” thread:
cde.AddCount(2);
// start worker threads...
cde.Wait();
“Worker” threads:
// do the work...
cde.Signal()
Pros | Good alternative to Semaphore |
---|---|
Cons | Can't work across processes like Semaphore |
Since | .NET Framework 4 |
Alt. | Barrier, Semaphore, SemaphoreSlim |
Links | MSDN: CountdownEvent Class |
11. EventWaitHandle
EventWaitHandle
is a newer version of both AutoResetEvent
and ManualResetEvent
(the “reset” behavior is determined by the enum EventResetMode
). It supports named event, so it can do signaling across different processes.
readonly EventWaitHandle eventWaitHandle =
new EventWaitHandle(false, EventResetMode.ManualReset);
// master thread
eventWaitHandle.WaitOne();
// slave threads
eventWaitHandle.Set();
Pros | Supports named events (cross process) |
---|---|
Cons | Slow (kernel mode) |
Since | .NET Framework 2.0 |
Alt. | AutoResetEvent, CancellationToken, ManualResetEvent, Monitor |
Links | MSDN: EventWaitHandle Class |
12. Interlocked operations
The Interlocked
class provides several static methods that implements atomic operations.
Add()
, Increment()
and Decrement()
can be used for int
and long
:
Interlocked.Add(ref field, 42); // field += 42;
Interlocked.Increment(ref field); // field++;
Interlocked.Decrement(ref field); // field--;
Exchange()
and CompareExchange()
can be used for int
, long
, float
,
double
, IntPtr
and any class
(but not struct
):
// var tmp = field;
// field = 42;
// return tmp;
Interlocked.Exchange(ref field, 42);
// var tmp = field;
// if (field == 21)
// field = 42;
// return tmp;
Interlocked.CompareExchange(ref field, 42, 21);
Finally, Read()
allows to read a long
in an atomic way.
Pros | Non blocking |
---|---|
Cons | Verbose |
Since | .NET Framework 1.0 |
Alt. | lock, Monitor, Volatile class, volatile keyword |
Links |
MSDN: Interlocked Methods |
13. Lock statement
The lock
statement is the most natural way to implement mutual exclusion in C#.
lock (syncObject)
{
// use shared resource...
}
Pros | Lock is released if exception is thrown |
---|---|
Cons | Has kwown pitfalls: lock a public object or a string |
Since | C# specifications 1.0 |
Alt. | Monitor, Mutex |
Links |
MSDN: lock Statement (C# Reference) Coverity: Can I skip the lock when reading an integer? |
14. ManualResetEvent
When signaled, a ManualResetEvent
releases all waiting threads. This is different from a ManualResetEvent
which releases exactly one waiting thread.
// master thread
manualResetEvent.WaitOne();
// slave threads
manualResetEvent.Set();
Pros | Resumes all waiting threads |
---|---|
Cons | Doesn't support named events |
Since | .NET Framework 1.0 |
Alt. | CancellationToken, EventWaitHandle, ManualResetEventSlim, SpinWait |
Links |
MSDN: ManualResetEvent Class MSDN: ManualResetEvent and ManualResetEventSlim |
15. ManualResetEventSlim
ManualResetEventSlim
is a lightweight version of ManualResetEvent
that provides better performance when the wait is short. It’s an hybrid construct: it first spin waits for a short time, then yields if still not signaled.
// master thread
manualResetEventSlim.Wait();
// slave threads
manualResetEventSlim.Set();
Pros | Performance for short waits |
---|---|
Cons | Not the same API as ManualResetEvent |
Since | .NET Framework 4.0 |
Alt. | CancellationToken, EventWaitHandle, ManualResetEvent, SpinWait |
Links |
MSDN: ManualResetEventSlim Class MSDN: ManualResetEvent and ManualResetEventSlim |
16. MethodImplOptions.Synchronized
The attribute MethodImplAttribute
can be set on methods. If MethodImplOptions.Synchronized
is specified, the method will be executed by only one thread at a time.
When applied to instance methods, it locks the instance, ie this
.
When applied to static methods, it locks the Type
.
Both case are nowadays considered as very bad practices.
[MethodImpl(MethodImplOptions.Synchronized)]
void SomeMethod()
{
// use shared resource...
}
This is equivalent to
void SomeMethod()
{
lock (this)
{
// use shared resource...
}
}
Pros | Short code, easy to read |
---|---|
Cons |
Locks the instance instead of a private field. Considered as a bad practice. |
Since | .NET Framework 1.1 |
Alt. | lock statement, Monitor |
Links |
MethodImplOptions Enumeration StackOverflow: What does MethodImplOptions.Synchronized do? StackOverflow: Manual locking vs Synchronized methods |
17. Monitor.Enter/Exit
The static methods Monitor.Enter()
and Monitor.Exit()
allows to make the same thing as the lock
statement.
Monitor.Enter(syncObject);
// use shared resource...
Monitor.Exit(syncObject);
In a real project, you would call Exit()
in a finally
block.
Pros | More flexible that the lock statement |
---|---|
Cons | Same pitfalls as the lock statement |
Since | .NET Framework 1.0 |
Alt. | AutoResetEvent, Mutex, lock statement, SpinLock |
Links |
MSDN: Monitor.Enter Method MSDN: Monitor.Exit Method |
18. Monitor.Pulse()
Monitor.Pulse()
method allows to wake a waiting thread and transfer the lock ownership from one thread to another.
Thread “Producer”:
lock (syncObject)
{
// produce some data...
Monitor.Pulse(syncObject);
}
Thread “Consumer”:
lock (syncObject)
{
Monitor.Wait(syncObject);
// consume the data...
}
Pros | Lock is constantly held |
---|---|
Cons | Very tricky |
Since | .NET Framework 1.0 |
Alt. | AutoResetEvent, ReaderWriterLock, ReaderWriterLockSlim |
Links | MSDN: Monitor.Pulse Method |
19. Monitor.PulseAll()
As the name suggests, PulseAll()
does the same thing as Pulse()
but instead of waking the next waiter, it wakes all of them.
Whereas Pulse()
is suited for a producer/consumer pattern where only one consumer can use the data, PulseAll()
would be useful when several consumer can use the data. In both case, the lock is held by only one thread at a time, so only one can run at a given moment.
Pros | Lock is constantly held |
---|---|
Cons | Even trickier than Pulse |
Since | .NET Framework 1.0 |
Alt. | ManualResetEvent, ManualResetEventSlim, ReaderWriterLock, ReaderWriterLockSlim |
Links | MSDN: Monitor.PulseAll Method |
20. Mutex
Mutex
provides mutual exclusion at the operating system level. Named Mutex
can be used from different processes.
readonly Mutex mutex = new Mutex();
mutex.WaitOne();
// use shared resource...
mutex.ReleaseMutex();
Pros | Can work accross processes |
---|---|
Cons | Slow (kernel mode) |
Since | .NET Framework 1.0 |
Alt. | lock statement, Monitor.Lock()/.Exit() |
Links | MSDN: Mutex Class |
21. ReaderWriterLock
ReaderWriterLock
is designed to be used when a single thread can write and several threads can read concurrently.
readonly ReaderWriterLock rwl = new ReaderWriterLock();
“Writer” threads:
rwl.AcquireWriterLock(timeout);
// write...
rwl.ReleaseWriterLock();
“Reader” threads:
rwl.AcquireReaderLock(timeout);
// read...
rwl.ReleaseWriterLock();
Pros | Clean dedicated solution |
---|---|
Cons | Replaced by ReaderWriterLockSlim in .NET 3.5 |
Since | .NET Framework 1.0 |
Alt. | ReaderWriterLockSlim, CondurrentQueue |
Links | MSDN: ReaderWriterLock Class |
22. ReaderWriterLockSlim
ReaderWriterLockSlim
is an upgraded version of ReaderWriterLock
with better performance and simpler rules.
readonly ReaderWriterLockSlim rwl = new ReaderWriterLockSlim();
“Writer” threads:
rwl.EnterWriteLock();
// write...
rwl.ExitWriteLock();
“Reader” threads:
rwl.EnterReadLock();
// read...
rwl.ExitReadLock();
Pros | Even better than ReaderWriterLock |
---|---|
Cons | None |
Since | .NET Framework 3.5 |
Alt. | ReaderWriterLock, ConcurrentQueue |
Links | MSDN: ReaderWriterLockSlim Class |
23. Semaphore
Semaphore
is used to limit the number of threads that access to a shared resources. Each time the semaphore is acquired, the counter decrements; when the counter is zero the thread is blocked. Named Semaphore
can be used from different processes.
readonly Semaphore semaphore = new Semaphore(START, MAX);
semaphore.WaitOne();
// use shared resource...
semaphore.Release();
Pros | Can work accross processes |
---|---|
Cons | Slow (kernel mode) |
Since | .NET Framework 2.0 |
Alt. | SemaphoreSlim, CountdownEvent |
Links | MSDN: Semaphore Class |
24. SemaphoreSlim
SemaphoreSlim
is a lightweight version of Semaphore
. It’s a user mode lock, no kernel object is involved.
readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
semaphore.Wait();
// use shared resource...
semaphore.Release();
Pros | Fast (user mode) |
---|---|
Cons | Not suitable for long waits. Can't work across processes |
Since | .NET Framework 4 |
Alt. | Semaphore, CountdownEvent |
Links |
MSDN: SemaphoreSlim Class StackOverflow: Choose between Semaphore and SemaphoreSlim |
25. SpinLock
SpinLock
is a lightweight version of Monitor.Enter/Exit
. It’s a user mode lock, no kernel object is involved.
SpinLock spinLock = new SpinLock();
bool lockTaken = false;
spinLock.Enter(ref lockTaken);
// use shared resource...
spinLock.Exit();
Pros | Light (it's a struct) and fast (user mode) |
---|---|
Cons | Not suitable for long waits |
Since | .NET Framework 4 |
Alt. | Monitor.Enter()/.Exit(), lock statement |
Links | MSDN: SpinLock Structure |
26. SpinWait
The SpinWait
class allows to block until a specified condition is met, ie until some function return true
. Despite the word “spin” in its name, SpinWait
is an hybrid construct and will yield when the wait is long.
The long version:
SpinWait spinWait = new SpinWait();
while (!someCondition)
{
sw.SpinOnce();
}
The short version:
SpinWait.SpinUntil(() => someCondition);
Pros | Yields CPU |
---|---|
Cons | None |
Since | .NET Framework 4 |
Alt. | CancellationToken, ManualResetEvent, ManualResetEventSlim |
Links | MSDN: SpinWait Structure |
27. Thread.MemoryBarrier
Thread.MemoryBarrier()
inserts a full memory barrier (aka full memory fence) which prevents the CLR or the processor from reordering the instructions.
Interlocked.MemoryBarrier
is a synonym for Thread.MemoryBarrier()
added in .NET framework 4.5.
// ensure 'first' is set before 'second'
first = true;
Thread.MemoryBarrier();
second = true;
Pros | Non blocking operation |
---|---|
Cons | Very tricky |
Since | C# 1.0 |
Alt. | Thread.VolatileRead(), Thread.VolatileWrite(), Volatile.Read(), Volatile.Write() |
Links |
MSDN: Thread.MemoryBarrier Method MSDN: Interlocked.MemoryBarrier Method Threading in C#, by Joe Albahari Wikipedia: Memory barrier |
28. Thread.VolatileRead()/Write()
Thread.VolatileRead()
and Thread.VolatileWrite()
guaranties that concurrent threads share an up-to-date version of a field, regardless of the number of processors or the state of processor cache.
These methods are obsolete and are now replaced by Volatile.Read()
and Volatile.Write()
. The difference is that the new versions insert half memory barrier instead of a full memory barrier, making them more efficient.
int myField;
// some thread
fieldValue = Thread.VolatileRead(ref myField);
// some other thread
Thread.VolatileWrite(ref myField, newValue);
Pros | Doesn't block the thread |
---|---|
Cons | Obsolete, replaced by Volatile class |
Since | .NET Framework 1.1 |
Alt. | volatile keywork, Volatile.Read(), Volatile.Write(), MemoryBarrier |
Links |
StackOverflow: Volatile vs VolatileRead/Write? MSDN: Thread.VolatileRead Method MSDN: Thread.VolatileWrite Method |
29. volatile keyword
When you declare a field as volatile
, the compiler assumes that the fields is accessed concurrently by several thread, and will disable optimization accordingly. AFAIK, this is the same thing as accessing the field via Volatile.Read()
and Volatile.Write()
.
Pros | Less error prone than Volatile class |
---|---|
Cons | Only works for fields of certains types |
Since | C# 1.0 |
Alt. | Volatile.Read(), Volatile.Write() |
Links |
MSDN: volatile (C# Reference) Threading in C#, by Joe Albahari Coverity: Reordering optimizations StackOverflow: What is the purpose of 'volatile' keyword in C# Eric Lippert's blog: Atomicity, volatility and immutability... Joe Duffy's Blog: Sayonara volatile |
30. Volatile.Read()/Write()
Volatile.Write()
ensures that a value written to a memory location is immediately visible to all processors. Volatile.Read()
obtains the very latest value written to a memory location by any processor.
int myField;
// some thread
fieldValue = Volatile.Read(ref myField);
// some other thread
Volatile.Write(ref myField, newValue);
Both methods insert a half memory barrier that prevents the processors from reordering the instructions:
- If a read or write appears after
Volatile.Read()
, the processor cannot move it up. - If a read or write appears before
Volatile.Write()
, the processor cannot move it down.
Pros | Doesn't block the thread |
---|---|
Cons | Very hard to get it 100% right |
Since | .NET Framework 4.5 |
Alt. | volative keyword, Thread.VolatileRead(), Thread.VolatileWrite() |
Links |
StackOverflow: Volatile vs VolatileRead/Write? MSDN: Volatile Class Eric Lippert's blog: Atomicity, volatility and immutability... Joe Duffy's Blog: Sayonara volatile |