Introduction
Synchronization in C# is crucial when dealing with multithreaded applications to ensure that multiple threads can access shared resources without causing data corruption or inconsistency. C# provides several synchronization primitives to manage access to shared resources, including locks, monitors, mutexes, semaphores, and more. These primitives help you coordinate the actions of multiple threads and ensure thread safety.
Key Synchronization Primitives
- lock Statement
- Monitor Class
- Mutex
- Semaphore
- AutoResetEvent and ManualResetEvent
- ReaderWriterLockSlim
1. lock Statement
The lock
statement is the simplest and most commonly used synchronization mechanism. It ensures that a block of code runs to completion without being interrupted by other threads.
Example
using System;
using System.Threading;
namespace LockExample
{
class Program
{
private static readonly object _lock = new object();
private static int _counter = 0;
static void Main(string[] args)
{
Thread thread1 = new Thread(IncrementCounter);
Thread thread2 = new Thread(IncrementCounter);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine($"Final counter value: {_counter}");
}
static void IncrementCounter()
{
for (int i = 0; i < 1000; i++)
{
lock (_lock)
{
_counter++;
}
}
}
}
}
Output
Final counter value: 2000
2. Monitor Class
The Monitor
class provides a more flexible way to lock a section of code. It offers methods like Enter
, Exit
, Wait
, and Pulse
to control the synchronization.
Example
using System;
using System.Threading;
namespace MonitorExample
{
class Program
{
private static readonly object _lock = new object();
private static int _counter = 0;
static void Main(string[] args)
{
Thread thread1 = new Thread(IncrementCounter);
Thread thread2 = new Thread(IncrementCounter);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine($"Final counter value: {_counter}");
}
static void IncrementCounter()
{
for (int i = 0; i < 1000; i++)
{
Monitor.Enter(_lock);
try
{
_counter++;
}
finally
{
Monitor.Exit(_lock);
}
}
}
}
}
Output
Final counter value: 2000
3. Mutex
A Mutex
is a synchronization primitive that can be used to manage access to a resource across multiple threads and even processes. Unlike lock
, which is limited to the current application domain, a Mutex
can be used system-wide.
Example
using System;
using System.Threading;
namespace MutexExample
{
class Program
{
private static readonly Mutex _mutex = new Mutex();
private static int _counter = 0;
static void Main(string[] args)
{
Thread thread1 = new Thread(IncrementCounter);
Thread thread2 = new Thread(IncrementCounter);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine($"Final counter value: {_counter}");
}
static void IncrementCounter()
{
for (int i = 0; i < 1000; i++)
{
_mutex.WaitOne(); // Acquire the mutex
try
{
_counter++;
}
finally
{
_mutex.ReleaseMutex(); // Release the mutex
}
}
}
}
}
Output
Final counter value: 2000
4. Semaphore
A Semaphore
controls access to a resource pool, allowing a specified number of threads to enter the critical section simultaneously. It is useful for limiting the number of concurrent accesses to a resource.
Example
using System;
using System.Threading;
namespace SemaphoreExample
{
class Program
{
private static readonly Semaphore _semaphore = new Semaphore(2, 2);
private static int _counter = 0;
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
Thread thread = new Thread(IncrementCounter);
thread.Start();
}
}
static void IncrementCounter()
{
_semaphore.WaitOne(); // Acquire the semaphore
try
{
for (int i = 0; i < 1000; i++)
{
_counter++;
}
Console.WriteLine($"Counter value: {_counter}");
}
finally
{
_semaphore.Release(); // Release the semaphore
}
}
}
}
Output
Counter value: 1000
Counter value: 2000
Counter value: 3000
Counter value: 4000
Counter value: 5000
5. AutoResetEvent and ManualResetEvent
AutoResetEvent
and ManualResetEvent
are signaling mechanisms to control thread execution. They are used to signal one or more waiting threads that an event has occurred.
Example
using System;
using System.Threading;
namespace EventExample
{
class Program
{
private static readonly AutoResetEvent _autoResetEvent = new AutoResetEvent(false);
static void Main(string[] args)
{
Thread workerThread = new Thread(DoWork);
workerThread.Start();
Console.WriteLine("Main thread waiting for worker thread to signal...");
_autoResetEvent.WaitOne(); // Wait for signal
Console.WriteLine("Main thread received signal. Continuing...");
}
static void DoWork()
{
Thread.Sleep(2000); // Simulate work
Console.WriteLine("Worker thread signaling main thread...");
_autoResetEvent.Set(); // Signal
}
}
}
Output
Main thread waiting for worker thread to signal...
Worker thread signaling main thread...
Main thread received signal. Continuing...
6. ReaderWriterLockSlim
ReaderWriterLockSlim
is a synchronization primitive that allows multiple threads to read a resource simultaneously but only one thread to write to it at a time. It is useful for scenarios where reads are much more frequent than writes.
Example
using System;
using System.Threading;
namespace ReaderWriterLockSlimExample
{
class Program
{
private static readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
private static int _counter = 0;
static void Main(string[] args)
{
Thread writerThread = new Thread(Write);
Thread readerThread1 = new Thread(Read);
Thread readerThread2 = new Thread(Read);
writerThread.Start();
readerThread1.Start();
readerThread2.Start();
writerThread.Join();
readerThread1.Join();
readerThread2.Join();
}
static void Write()
{
_rwLock.EnterWriteLock();
try
{
_counter++;
Console.WriteLine($"Counter incremented to {_counter}");
}
finally
{
_rwLock.ExitWriteLock();
}
}
static void Read()
{
_rwLock.EnterReadLock();
try
{
Console.WriteLine($"Counter value read as {_counter}");
}
finally
{
_rwLock.ExitReadLock();
}
}
}
}
Output
Counter incremented to 1
Counter value read as 1
Counter value read as 1
Conclusion
Synchronization in C# is essential for ensuring that multiple threads can access shared resources without causing data corruption or inconsistency. The lock
statement, Monitor
class, Mutex
, Semaphore
, AutoResetEvent
, ManualResetEvent
, and ReaderWriterLockSlim
are powerful synchronization primitives that help you manage thread synchronization effectively. By understanding and using these synchronization mechanisms, you can write more reliable and thread-safe multithreaded applications in C#.