Appearance
.NET Concurrency
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.SynchronizedmethodAutoResetEventclassBarrierclassBlockingCollectionclassCancellationTokenstructConcurrentBagclassConcurrentDictionaryclassConcurrentQueueclassConcurrentStackclassCountdownEventclassEventWaitHandleclassInterlockedoperationslockstatementManualResetEventclassManualResetEventSlimclassMethodImplOptions.SynchronizedattributeMonitor.Enterand.ExitmethodsMonitor.PulsemethodMonitor.PulseAllmethodMutexclassReaderWriterLockclassReaderWriterLockSlimclassSemaphoreclassSemaphoreSlimclassSpinLockclassSpinWaitclassThread.MemoryBarriermethod (andInterlocked.MemoryBarrier)Thread.VolatileReadand.VolatileWritemethodsvolatilekeywordVolatile.ReadandWritemethods
1. ArrayList.Synchronized
For those who still use ArrayList, there is a simple way to make it thread safe:
csharp
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:
csharp
lock(array.SyncRoot)
{
foreach (object item in array)
{
// ...
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
- 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.
csharp
readonly AutoResetEvent autoResetEvent = new AutoResetEvent(true);
autoResetEvent.WaitOne();
// ...insert code here...
autoResetEvent.Set();1
2
3
4
5
2
3
4
5
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:
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.
csharp
while (someCondition)
{
// ...insert code here...
barrier.SignalAndWait();
}1
2
3
4
5
6
2
3
4
5
6
- 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:
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.
csharp
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();1
2
3
4
5
6
7
2
3
4
5
6
7
- 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.
csharp
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...
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- Pros: Clear purpose: cancel
- Cons: None
- Since: .NET Framework 4
- Alt.:
ManualResetEvent - Links:
6. ConcurrentBag
ConcurrentBag is a thread-safe unordered collection.
csharp
bag = new ConcurrentBag<T>();
bag.Add(value);
bag.TryPeek(out value);
bag.TryTake(out value);1
2
3
4
5
2
3
4
5
- 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.
csharp
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);1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
- 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.
csharp
queue = new ConcurrentQueue<T>();
queue.Enqueue(value);
queue.TryPeek(out value);
queue.TryDequeue(out value);1
2
3
4
5
2
3
4
5
- 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.
csharp
stack = new ConcurrentStack<T>();
stack.Push(value);
stack.TryPeek(out value);
stack.TryPop(out value);1
2
3
4
5
2
3
4
5
- 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.
csharp
readonly CountdownEvent cde = new CountdownEvent(0);“Main” thread:
csharp
cde.AddCount(2);
// start worker threads...
cde.Wait();1
2
3
2
3
“Worker” threads:
csharp
// do the work...
cde.Signal()1
2
2
- 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.
csharp
readonly EventWaitHandle eventWaitHandle =
new EventWaitHandle(false, EventResetMode.ManualReset);
// master thread
eventWaitHandle.WaitOne();
// slave threads
eventWaitHandle.Set();1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
- 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:
csharp
Interlocked.Add(ref field, 42); // field += 42;
Interlocked.Increment(ref field); // field++;
Interlocked.Decrement(ref field); // field--;1
2
3
2
3
Exchange() and CompareExchange() can be used for int, long, float, double, IntPtr and any class (but not struct):
csharp
// 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);1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Finally, Read() allows to read a long in an atomic way.
- Pros: Non blocking
- Cons: Verbose
- Since: .NET Framework 1.0
- Alt.:
lock,Monitor,Volatileclass,volatilekeyword - Links: MSDN: Interlocked Methods
13. Lock statement
The lock statement is the most natural way to implement mutual exclusion in C#.
csharp
lock (syncObject)
{
// use shared resource...
}1
2
3
4
2
3
4
- 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:
14. ManualResetEvent
When signaled, a ManualResetEvent releases all waiting threads. This is different from a AutoResetEvent which releases exactly one waiting thread.
csharp
// master thread
manualResetEvent.WaitOne();
// slave threads
manualResetEvent.Set();1
2
3
4
5
2
3
4
5
- Pros: Resumes all waiting threads
- Cons: Doesn't support named events
- Since: .NET Framework 1.0
- Alt.:
CancellationToken,EventWaitHandle,ManualResetEventSlim,SpinWait - Links:
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.
csharp
// master thread
manualResetEventSlim.Wait();
// slave threads
manualResetEventSlim.Set();1
2
3
4
5
2
3
4
5
- Pros: Performance for short waits
- Cons: Not the same API as ManualResetEvent
- Since: .NET Framework 4.0
- Alt.:
CancellationToken,EventWaitHandle,ManualResetEvent,SpinWait - Links:
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.
csharp
[MethodImpl(MethodImplOptions.Synchronized)]
void SomeMethod()
{
// use shared resource...
}1
2
3
4
5
2
3
4
5
This is equivalent to
csharp
void SomeMethod()
{
lock (this)
{
// use shared resource...
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
- 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.:
lockstatement,Monitor - Links:
17. Monitor.Enter/Exit
The static methods Monitor.Enter() and Monitor.Exit() allows to make the same thing as the lock statement.
csharp
Monitor.Enter(syncObject);
// use shared resource...
Monitor.Exit(syncObject);1
2
3
2
3
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,lockstatement,SpinLock - Links:
18. Monitor.Pulse()
Monitor.Pulse() method allows to wake a waiting thread and transfer the lock ownership from one thread to another.
Thread “Producer”:
csharp
lock (syncObject)
{
// produce some data...
Monitor.Pulse(syncObject);
}1
2
3
4
5
2
3
4
5
Thread “Consumer”:
csharp
lock (syncObject)
{
Monitor.Wait(syncObject);
// consume the data...
}1
2
3
4
5
2
3
4
5
- 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.
csharp
readonly Mutex mutex = new Mutex();
mutex.WaitOne();
// use shared resource...
mutex.ReleaseMutex();1
2
3
4
5
2
3
4
5
- Pros: Can work accross processes
- Cons: Slow (kernel mode)
- Since: .NET Framework 1.0
- Alt.:
lockstatement,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.
csharp
readonly ReaderWriterLock rwl = new ReaderWriterLock();“Writer” threads:
csharp
rwl.AcquireWriterLock(timeout);
// write...
rwl.ReleaseWriterLock();1
2
3
2
3
“Reader” threads:
csharp
rwl.AcquireReaderLock(timeout);
// read...
rwl.ReleaseWriterLock();1
2
3
2
3
- Pros: Clean dedicated solution
- Cons: Replaced by
ReaderWriterLockSlimin .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.
csharp
readonly ReaderWriterLockSlim rwl = new ReaderWriterLockSlim();“Writer” threads:
csharp
rwl.EnterWriteLock();
// write...
rwl.ExitWriteLock();1
2
3
2
3
“Reader” threads:
csharp
rwl.EnterReadLock();
// read...
rwl.ExitReadLock();1
2
3
2
3
- 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.
csharp
readonly Semaphore semaphore = new Semaphore(START, MAX);
semaphore.WaitOne();
// use shared resource...
semaphore.Release();1
2
3
4
5
2
3
4
5
- 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.
csharp
readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
semaphore.Wait();
// use shared resource...
semaphore.Release();1
2
3
4
5
2
3
4
5
- Pros: Fast (user mode)
- Cons: Not suitable for long waits. Can't work across processes
- Since: .NET Framework 4
- Alt.:
Semaphore,CountdownEvent - Links:
25. SpinLock
SpinLock is a lightweight version of Monitor.Enter / Exit. It's a user mode lock, no kernel object is involved.
csharp
SpinLock spinLock = new SpinLock();
bool lockTaken = false;
spinLock.Enter(ref lockTaken);
// use shared resource...
spinLock.Exit();1
2
3
4
5
6
2
3
4
5
6
- Pros: Light (it's a struct) and fast (user mode)
- Cons: Not suitable for long waits
- Since: .NET Framework 4
- Alt.:
Monitor.Enter()/.Exit(),lockstatement - 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:
csharp
SpinWait spinWait = new SpinWait();
while (!someCondition)
{
sw.SpinOnce();
}1
2
3
4
5
2
3
4
5
The short version:
csharp
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.
csharp
// ensure 'first' is set before 'second'
first = true;
Thread.MemoryBarrier();
second = true;1
2
3
4
2
3
4
- Pros: Non blocking operation
- Cons: Very tricky
- Since: C# 1.0
- Alt.:
Thread.VolatileRead(),Thread.VolatileWrite(),Volatile.Read(),Volatile.Write() - Links:
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.
csharp
int myField;
// some thread
fieldValue = Thread.VolatileRead(ref myField);
// some other thread
Thread.VolatileWrite(ref myField, newValue);1
2
3
4
5
6
7
2
3
4
5
6
7
- Pros: Doesn't block the thread
- Cons: Obsolete, replaced by Volatile class
- Since: .NET Framework 1.1
- Alt.:
volatilekeywork,Volatile.Read(),Volatile.Write(),MemoryBarrier - Links:
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:
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.
csharp
int myField;
// some thread
fieldValue = Volatile.Read(ref myField);
// some other thread
Volatile.Write(ref myField, newValue);1
2
3
4
5
6
7
2
3
4
5
6
7
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.:
volativekeyword,Thread.VolatileRead(),Thread.VolatileWrite() - Links: