C# Thread Synchronization
In multithreaded applications, multiple threads often access shared data or resources simultaneously. This can cause race conditions, data inconsistency and unpredictable behavior. Thread synchronization in C# ensures that threads coordinate properly, preventing conflicts and maintaining correctness.
Why Thread Synchronization?
- Avoids race conditions
- Maintains thread safety
- Ensures predictable program behavior
Synchronization ensures thread safety but may reduce performance if overused, as threads spend time waiting for locks to be released.
Synchronized Blocks in C#
In C#, synchronized blocks are written using the lock keyword on a specific object. Only one thread can enter the block at a time. Other threads are blocked until the lock is released.
Syntax:
lock(sync_object) {
// Access shared resources
}
Example:
class Counter {
private int c = 0; // Shared variable
public void Inc() {
lock (this) { // Synchronize only this block
c++;
}
}
public int Get() {
return c;
}
}
Synchronize Threads in C#
Threads can be synchronized in different ways depending on the requirement. C# provides multiple constructs to handle synchronization:
Using lock
The lock keyword is the most common way to synchronize threads. It restricts access to a code block so only one thread can execute it at a time.
using System;
using System.Threading;
class Program
{
private static int counter = 0;
private static readonly object lockObj = new object();
static void Increment()
{
for (int i = 0; i < 5; i++)
{
lock (lockObj)
{
counter++;
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} -> {counter}");
}
Thread.Sleep(100);
}
}
static void Main()
{
Thread t1 = new Thread(Increment);
Thread t2 = new Thread(Increment);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
}
Output
Thread 3 -> 1 Thread 4 -> 2 Thread 3 -> 3 Thread 4 -> 4 Thread 3 -> 5 Thread 4 -> 6 Thread 3 -> 7 Thread 4 -> 8 Thread 3 -> 9 Thread 4 -> 10
Explanation: Here, both threads attempt to increment counter. Without synchronization, values may overlap or skip. The lock ensures only one thread updates the counter at a time.
Using Monitor Class
The Monitor class provides a more flexible way of synchronizing threads. It works similarly to lock but offers additional control like Wait(), Pulse(), and PulseAll().
class Demo
{
private static int count = 0;
private static readonly object sync = new object();
static void Increment()
{
for (int i = 0; i < 3; i++)
{
Monitor.Enter(sync);
try
{
count++;
Console.WriteLine($"Count = {count}");
}
finally
{
Monitor.Exit(sync);
}
}
}
}
Explanation: Monitor.Enter and Monitor.Exit provide explicit control. The try-finally ensures the lock is released even if an exception occurs.
Using Mutex
Mutex is used to synchronize threads across multiple processes. Unlike lock, it works not only within a single application but also across different applications.
class Demo
{
private static Mutex mutex = new Mutex();
static void PrintNumbers()
{
mutex.WaitOne();
for (int i = 1; i <= 3; i++)
{
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} -> {i}");
Thread.Sleep(200);
}
mutex.ReleaseMutex();
}
}
Explanation: Here, WaitOne() acquires the mutex and ReleaseMutex() releases it. Only one thread across processes can hold the mutex at a time.
Using Semaphore
A Semaphore controls access to a resource by allowing a specified number of threads to enter at once. This is useful when you want to limit concurrent access.
class Demo
{
private static Semaphore semaphore = new Semaphore(2, 2);
static void Work()
{
semaphore.WaitOne();
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} entered");
Thread.Sleep(500);
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} exiting");
semaphore.Release();
}
}
Explanation: Here, a maximum of 2 threads can work simultaneously. Others wait until a slot is released.
Using SemaphoreSlim
SemaphoreSlim is a lightweight alternative to Semaphore and is recommended for synchronization within a single process.
class Demo
{
private static SemaphoreSlim sem = new SemaphoreSlim(2, 2);
static async Task Work()
{
await sem.WaitAsync();
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} entered");
await Task.Delay(500);
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} exiting");
sem.Release();
}
}
Explanation: It supports asynchronous programming and is more efficient when inter-process synchronization is not required.