Anton Baksheiev
4 min readJul 26, 2024

Synchronization Primitives .Net/c#

In numerous interviews, I’ve observed that many people are unfamiliar with the extensive range of multitasking tools available in .NET. This article aims to consolidate these tools into one comprehensive resource, providing both a refresher for those already familiar with them and an opportunity to learn something new. I will explore various synchronization mechanisms available in .NET, including practical examples of their usage.

By the end of this article, I will also include a performance comparison of these synchronization methods. This comparison will be invaluable for selecting the most appropriate synchronization technique, especially when performance is a critical factor.

  1. ‘Lock’
  • Simplifies the use of Monitor class.
  • Ensures that only one thread can enter the critical section at a time.
private static readonly object lockObject = new object();

public void DoWork()
{
lock (lockObject)
{
// Critical section
}
}

2. ‘Monitor’

  • Provides a mechanism that synchronizes access to objects.
  • More control compared to lock, with methods like Enter, Exit, Wait, and Pulse.
Monitor.Enter(lockObject);
try
{
// Critical section
}
finally
{
Monitor.Exit(lockObject);
}

3. ‘Mutex’

  • Can be used for inter-process synchronization.
  • Allows one thread at a time to access the critical section.
private static Mutex mutex = new Mutex();

public void DoWork()
{
mutex.WaitOne();
try
{
// Critical section
}
finally
{
mutex.ReleaseMutex();
}
}

4. ‘Semaphore’

  • Limits the number of threads that can access a resource concurrently.
private static Semaphore semaphore = new Semaphore(3, 3); // Max 3 threads

public void DoWork()
{
semaphore.WaitOne();
try
{
// Critical section
}
finally
{
semaphore.Release();
}
}

5. ‘SemaphoreSlim’

  • Lightweight version of Semaphore for intra-process synchronization.
private static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(3, 3); // Max 3 threads

public async Task DoWorkAsync()
{
await semaphoreSlim.WaitAsync();
try
{
// Critical section
}
finally
{
semaphoreSlim.Release();
}
}

6. ‘AutoResetEvent’

  • Notifies a waiting thread that an event has occurred, automatically resets after releasing a single thread.
private static AutoResetEvent autoResetEvent = new AutoResetEvent(false);

public void DoWork()
{
autoResetEvent.WaitOne();
// Critical section
autoResetEvent.Set();
}

7. ‘ManualResetEvent’

  • Notifies one or more waiting threads that an event has occurred, remains signaled until manually reset.
private static ManualResetEvent manualResetEvent = new ManualResetEvent(false);

public void DoWork()
{
manualResetEvent.WaitOne();
// Critical section
manualResetEvent.Set();
}

8. ‘ManualResetEventSlim’

  • Lightweight version of ManualResetEvent for better performance in scenarios with short wait times.
private static ManualResetEventSlim manualResetEventSlim = new ManualResetEventSlim(false);

public void DoWork()
{
manualResetEventSlim.Wait();
// Critical section
manualResetEventSlim.Set();
}

9. ‘Barrier’

  • Enables multiple threads to work concurrently until they all reach a certain point (barrier), then they can proceed.
private static Barrier barrier = new Barrier(3);

public void DoWork()
{
// Some work
barrier.SignalAndWait();
// Continue work after all threads reach the barrier
}

10. ‘ReaderWriterLockSlim

  • Allows multiple threads for reading or exclusive access for writing.
private static ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();

public void Read()
{
rwLock.EnterReadLock();
try
{
// Read operation
}
finally
{
rwLock.ExitReadLock();
}
}

public void Write()
{
rwLock.EnterWriteLock();
try
{
// Write operation
}
finally
{
rwLock.ExitWriteLock();
}
}

Benchmark Results

Having explored the various synchronization primitives available in .NET, including Lock, Mutex, Semaphore and AutoResetEvent, it is crucial to understand how they perform under different conditions. Each of these synchronization tools offers unique features and trade-offs that can significantly impact application performance. For example, while Lock (implemented with the Monitor class) is known for its simplicity and efficiency in most scenarios, Mutex provides cross-process synchronization but with additional overhead. Similarly, Semaphore and AutoResetEvent offer different mechanisms for signaling and waiting, which can influence their performance in multi-threaded applications.

The following table provides a detailed performance comparison of these synchronization primitives. By examining the results, you can gain insights into their relative efficiency in various scenarios and make data-driven decisions for optimizing concurrency control in your applications. This analysis not only highlights the strengths and weaknesses of each primitive but also helps in choosing the right tool for specific use cases based on their performance metrics.

|                  Method |       Mean |      Error |     StdDev |
|------------------------ |-----------:|-----------:|-----------:|
| Lock | 7.646 ms | 0.6796 ms | 0.3554 ms |
| Monitor | 6.661 ms | 0.7108 ms | 0.4230 ms |
| Mutex | 471.731 ms | 32.1785 ms | 19.1489 ms |
| Semaphore | 511.069 ms | 39.8801 ms | 23.7320 ms |
| SemaphoreSlim | 7.837 ms | 0.4154 ms | 0.2748 ms |
| AutoResetEvent | 541.058 ms | 71.1162 ms | 42.3201 ms |
| ManualResetEvent | 22.083 ms | 2.0857 ms | 1.3796 ms |
| ManualResetEventSlim | 7.536 ms | 0.2880 ms | 0.1714 ms |
| Barrier | 11.948 ms | 0.8099 ms | 0.5357 ms |
| ReaderWriterLockSlim | 3.628 ms | 0.2738 ms | 0.1629 ms |

// * Legends *
Mean : Arithmetic mean of all measurements
Error : Half of 99.9% confidence interval
StdDev : Standard deviation of all measurements
1 ms : 1 Millisecond (0.001 sec

Thank you for reading! If you have any questions or suggestions, feel free to connect with me on LinkedIn.

Anton Baksheiev

Software engineer specializing in quality and testing. AWS certified. Expert in .NET, databases, Docker, and TDD. Passionate about innovation and performance.