.NET C# synchronization primitives cheatsheet for interview
Hey there! As someone who has participated a fair share of interviews, I can tell you that synchronization primitives often pop up in technical discussions. So, let’s dive into the exciting world of C# synchronization primitives, exploring Mutex, Monitor, SemaphoreSlim, AutoResetEvent, ManualResetEvent, and EventWaitHandler, and what is also being asked frequently — differences between them. I will also provide more extended examples of AutoResetEvent and ManualResetEvent, since people always struggle to understand these *ResetEvent concepts and the difference between the two.
🟢Mutex
- Stands for “mutual exclusion”.
- A synchronization primitive used to protect access to shared resources.
- Can be used for inter-process synchronization (across multiple processes).
using System.Threading;
class MutexExample {
private static Mutex mutex = new Mutex();
public static void SharedResourceAccess() {
mutex.WaitOne(); // Acquire the mutex lock.
// Access shared resource here.
// Only one thread will have access to this part of code
// until .ReleaseMutex() called
// All other threads will be waiting on .WaitOne() line
mutex.ReleaseMutex(); // Release the mutex lock.
}
}
A few words about “interprocess communication”
Mutex can be used for interprocess communication by providing a unique name for the Mutex. When multiple processes (not even .NET process that your program created, but any other process in OS) use the same Mutex name, they share the same Mutex, allowing them to synchronize access to a shared resource across process boundaries.
Here’s an example of how to use a named Mutex for interprocess communication:
using System;
using System.Threading;
class NamedMutexExample {
private const string MutexName = "Global\\MyNamedMutex";
private static Mutex mutex;
public static void Main() {
bool createdNew;
using (mutex = new Mutex(false, MutexName, out createdNew)) {
if (createdNew) {
Console.WriteLine("Mutex created by this process.");
} else {
Console.WriteLine("Mutex is already created by another process.");
}
// Acquire the Mutex lock.
mutex.WaitOne();
// Access shared resource or perform inter-process communication here.
DoSomething();
// Release the Mutex lock.
mutex.ReleaseMutex();
}
}
}
🟢Monitor (also known as ‘lock’ statement)
- Synchronization primitive used to protect access to shared resources.
- Lighter weight compared to Mutex.
- Can only be used for intra-process synchronization (within the same process).
- Uses the
lock
keyword as a syntactic shortcut.
class MonitorExample {
private static object _lock = new object();
public static void SharedResourceAccess() {
lock (_lock) { // Monitor.Enter(_lock) is called implicitly.
// Access shared resource here.
// until .ReleaseMutex() called
// All other threads will be waiting on .WaitOne() line
} // Monitor.Exit(_lock) is called implicitly.
}
}
Difference between Mutex and Monitor
- Mutex can be used for inter-process synchronization, while Monitor can only be used for intra-process synchronization.
- Mutex is heavier than Monitor (by heavier I mean that it consumes uses more CPU cycles during acquiring and releasing the lock), making Monitor more suitable for cases where synchronization is only needed within a single process.
🟢SemaphoreSlim
- A lightweight synchronization primitive used to control access to a shared resource.
- Can be used to limit the number of concurrent threads.
- Supports the async/await pattern.
using System.Threading;
using System.Threading.Tasks;
class SemaphoreSlimExample {
private static SemaphoreSlim semaphore = new SemaphoreSlim(3); // Allow 3 concurrent threads.
public async static Task SharedResourceAccess() {
await semaphore.WaitAsync(); // Acquire the semaphore.
// Access shared resource here.
// Only three threads!!! can go through this part of code simultaneously.
// If there is a forth thread - it'll be waiting for the .Release()
// on .WaitAsync() line
semaphore.Release(); // Release the semaphore.
}
}
Difference between SemaphoreSlim and Semaphore
While SemaphoreSlim and Semaphore are both synchronization primitives used to control access to shared resources by limiting the number of concurrent threads. However, there are some key differences between them:
- Lightweight Implementation: SemaphoreSlim is a lightweight version of Semaphore, designed for better performance in scenarios where wait times are expected to be short. It is optimized for low-contention scenarios and uses spin-waiting to minimize context switches.
- Intra-Process Synchronization: SemaphoreSlim is limited to synchronization within a single process (intra-process). It cannot be used for inter-process synchronization, which means it cannot be shared across multiple processes.
- Asynchronous Support: SemaphoreSlim supports the async/await pattern, providing asynchronous methods like WaitAsync(), which enables non-blocking synchronization in asynchronous programming.
On the other hand, Semaphore:
- Heavier Implementation: Semaphore has a heavier implementation compared to SemaphoreSlim, which may lead to reduced performance when wait times are short.
- Inter-Process Synchronization: Semaphore can be used for both intra-process and inter-process synchronization. By providing a name for the Semaphore, it can be shared across multiple processes.
- No Asynchronous Support: Semaphore does not have built-in support for async/await pattern and does not provide asynchronous methods.
In summary, SemaphoreSlim is a lighter, more performant option for intra-process synchronization with async/await support, while Semaphore can be used for both intra-process and inter-process synchronization but lacks asynchronous support.
ManualResetEvent and AutoResetEvent
First of all let’s understand what “signaled state” is.
The “signaled state” refers to the condition of a synchronization primitive, such as an EventWaitHandle, ManualResetEvent, AutoResetEvent, or Semaphore. When a synchronization primitive is in the signaled state, it indicates that the conditions required for threads to continue execution have been met. Threads that are waiting for this condition (using methods like WaitOne()) can then proceed.
In the context of event synchronization primitives like ManualResetEvent and AutoResetEvent:
- Signaled state: The event has been set, allowing waiting threads to proceed. In the case of ManualResetEvent, all waiting threads are released. In the case of AutoResetEvent, one waiting thread is released and the event is automatically reset to the non-signaled state.
- Non-signaled state: The event has not been set or has been reset, causing threads that call WaitOne() to block until the event becomes signaled.
Synchronization primitives use these states to coordinate the execution of multiple threads and ensure that certain conditions are met before threads can access shared resources or continue processing.
You can switch between signaled and non-signaled states for both ManualResetEvent and AutoResetEvent using the Set() and Reset() methods. The main difference between these two synchronization primitives is how they behave when releasing waiting threads.
ManualResetEvent:
- Signaled state: Call the Set() method. This will release all waiting threads at once. The ManualResetEvent will remain in the signaled state until Reset() is called.
- Non-signaled state: Call the Reset() method. This will set the event back to the non-signaled state, causing any subsequent threads calling WaitOne() to block until the event is signaled again.
AutoResetEvent:
- Signaled state: Call the Set() method. This will release a single waiting thread and automatically reset the event to the non-signaled state. If there are multiple threads waiting, you’ll need to call Set() multiple times, once for each waiting thread.
- Non-signaled state: The AutoResetEvent automatically resets to the non-signaled state after releasing a waiting thread when Set() is called. You can also explicitly call the Reset() method to set the event back to the non-signaled state, but this is typically not necessary as it resets automatically.
In summary, both ManualResetEvent and AutoResetEvent use Set() to switch to the signaled state and Reset() to switch to the non-signaled state. The key difference is that ManualResetEvent releases all waiting threads when signaled and stays in the signaled state until manually reset, while AutoResetEvent releases only one waiting thread and automatically resets to the non-signaled state.
🟢ManualResetEvent
- A synchronization primitive that is signaled when a thread completes its work.
- Must be manually reset to an unsignaled state after releasing waiting threads.
- Useful when you need to notify multiple waiting threads of a completed task.
using System;
using System.Threading;
class ManualResetEventExample {
private static ManualResetEvent manualResetEvent = new ManualResetEvent(false);
public static void WorkerThread(object id) {
int threadId = (int)id;
Console.WriteLine($"Worker thread {threadId} waiting.");
manualResetEvent.WaitOne();
Console.WriteLine($"Worker thread {threadId} released.");
}
public static void Main() {
// Start three worker threads.
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(WorkerThread);
thread.Start(i);
}
// Release one worker thread at a time by calling Set() followed by Reset().
for (int i = 0; i < 3; i++) {
Console.WriteLine("Press ENTER to release one worker thread.");
Console.ReadLine();
manualResetEvent.Set(); // Signal the event, releasing waiting threads.
manualResetEvent.Reset(); // Reset the event to non-signaled state.
}
Console.ReadLine();
}
}
🟢AutoResetEvent
- A synchronization primitive that is signaled when a thread completes its work.
- Automatically resets to an unsignaled state after releasing a waiting thread.
- Useful when you need to notify a single waiting thread of a completed task.
using System;
using System.Threading;
class AutoResetEventExample {
private static AutoResetEvent autoResetEvent = new AutoResetEvent(false);
public static void WorkerThread(object id) {
int threadId = (int)id;
Console.WriteLine($"Worker thread {threadId} waiting.");
autoResetEvent.WaitOne();
Console.WriteLine($"Worker thread {threadId} released.");
}
public static void Main() {
// Start three worker threads.
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(WorkerThread);
thread.Start(i);
}
// Release one worker thread at a time by calling Set().
for (int i = 0; i < 3; i++) {
Console.WriteLine("Press ENTER to release one worker thread.");
Console.ReadLine();
autoResetEvent.Set(); // Signal the event, releasing one waiting thread.
// Don't need to call .Reset() here!
}
Console.ReadLine();
}
}
🟢EventWaitHandler
- A synchronization primitive that provides both auto-reset and manual-reset event functionality. (You can simulate both AutoResetEvent and ManualResetEvent using EventWaitHandler by changing the EventResetMode parameter in the constructor)
- Can be used for signaling between threads or processes.
- Inherits from WaitHandle, and both AutoResetEvent and ManualResetEvent are derived from EventWaitHandle.
Simulating AutoResetEvent using EventWaitHandler:
using System.Threading;
class SimulateAutoResetEventExample {
private static EventWaitHandler simulatedAutoResetEvent = new EventWaitHandler(false, EventResetMode.AutoReset);
public static void WorkerThread() {
// Perform work here.
simulatedAutoResetEvent.Set(); // Signal the completion of work.
}
public static void MainThread() {
simulatedAutoResetEvent.WaitOne(); // Wait for the worker thread to complete.
// Continue with the main thread.
}
}
Simulating ManualResetEvent using EventWaitHandler:
using System.Threading;
class SimulateManualResetEventExample {
private static EventWaitHandler simulatedManualResetEvent = new EventWaitHandler(false, EventResetMode.ManualReset);
public static void WorkerThread() {
// Perform work here.
simulatedManualResetEvent.Set(); // Signal the completion of work.
}
public static void MainThread() {
simulatedManualResetEvent.WaitOne(); // Wait for the worker thread to complete.
// Continue with the main thread.
}
public static void ResetEvent() {
simulatedManualResetEvent.Reset(); // Reset the event manually.
}
}
That’s it!
Feel free to share the next topic you want to hear about or you struggle with.