Thread synchronization in multithreading is the mechanism used to control multiple threads when they access shared resources at the same time.In a multithreaded application, it is required to coordinate with the running threads to avoid deadlock conditions.
Here in this blog, we will learn what is synchronization or thread synchronization in C#.
What is Synchronization?
Synchronization is a technique that provides thread safety. Thread safety means that when two or more threads need to access a shared resource, only one thread can access the resource at a time. The other threads must wait until the current thread has completed its operation and releases the resource.
Why is Synchronization needed?
In the multithreaded application when two or more threads are running, there is a chance that multiple threads may try to access to same resource at the same time.
Example-1If you have two threads, one that reads a text file and other threads writes to the text file, in this case both threads will try to access the text file at the same time. The first thread will try to read data from the text file, at the same time the second thread will try to write. This will lead to never ended task or deadlock.
Example-2Suppose two threads access the same variable:
counter = 0
Thread 1: counter = counter + 1
Thread 2: counter = counter + 1
You might expect:
counter = 2
But both threads could read counter = 0 before either writes the result:
Thread 1 → reads 0
Thread 2 → reads 0
Thread 1 → writes 1
Thread 2 → writes 1
Final counter = 1 ❌
This is called a race condition or Deadlock.
Deadlock is a situation when a thread is waiting for an object lock, that is acquired by another thread and second thread is waiting for an object lock that is acquired by first thread. Since, both threads are waiting for each other to release the lock, the condition is called deadlock. Deadlock is common thing; you can say it is a part of multithreading.
To avoid this deadlock condition in multithreading C# enables you to coordinate the action of multiple threads by using synchronized methods or synchronized statements.
How the Thread Synchronization Works
Thread synchronization works based on the concept of monitoring. A monitor is an object that is used as lock to the data member and methods of a class. If any object or resource associates with monitor, then it ensures that only one thread has access to the resource at any given time.
An object is said to have entered the monitor when a thread acquires a lock. When a synchronized method starts execution, the resource or object is locked until the method completes its task and automatically when method completes the task. During the execution of synchronized method, the object is locked so that no other synchronized method ca be invoked.
Thread synchronization can be achieved using the following synchronization statements or mechanisms.
- Lock Statement
The Lock statement ensures that when an object or method of a class inters in to monitor state by a thread then no other thread acquire the object until the current thread releases the object. The object automatically will be released when the thread execution completes.
Simple exampleclass Counter { private int count = 0; private readonly object lockObject = new object(); public void Increment() { lock (lockObject) { count++; } } public int GetCount() { lock (lockObject) { return count; } } }If multiple threads call
Example with multiple threadsIncrement()simultaneously, the lock ensures that the count++ operation is performed by one thread at a time.class Program { static int count = 0; static readonly object lockObject = new object(); static void Increment() { for (int i = 0; i < 100000; i++) { lock (lockObject) { count++; } } } static void Main() { Thread t1 = new Thread(Increment); Thread t2 = new Thread(Increment); t1.Start(); t2.Start(); t1.Join(); t2.Join(); Console.WriteLine(count); } }Without
How it workslock, you might expect200000, but due to a race condition, the result can be less than200000.Withlock, access tocount++is synchronized, so the result will be200000- A lock object is created
lockObject - A thread reaches the
lockstatement and tries to acquire the locklock (lockObject) { count++; }- If no other thread owns the lock → it gets the lock immediately.
- If another thread already owns it → the thread waits.
- Only one thread can enter the locked section. Multiple threads cannot execute the section simultaneously using the same lock object.
- The thread performs the critical operation
Thread t1 → gets lock → executes code Thread t2 → waits - When the thread leaves the lock block, the lock is released
Thread t1 → releases lock Thread t2 → waits - Another waiting thread can then acquire the lock
\
Thread t2 → gets lock → executes code - If an exception occurs, the lock is still released
Important:
lockusually create a private object specifically for locking, avoid locking on publicly accessible objects such asthis,typeof(MyClass), or strings. - A lock object is created
-
Monitor
Monitor work as like lock but it provides more control over the synchronization of various threads trying to access the same lock of code. For example when lock is applied by a thread to a object then the lock on object will be released automatically when thread execution completes but in the case of monitor manually lock need to be released when lock on object is no longer required.
Exampleusing System; using System.Threading; class Counter { private int count = 0; private readonly object lockObject = new object(); public void Increment() { Monitor.Enter(lockObject); try { count++; Console.WriteLine($"Count: {count}"); } finally { Monitor.Exit(lockObject); } } } class Program { static void Main() { Counter counter = new Counter(); Thread t1 = new Thread(counter.Increment); Thread t2 = new Thread(counter.Increment); t1.Start(); t2.Start(); t1.Join(); t2.Join(); } }
How it worksMonitor.Enter()→ acquires the lock.- The code inside
try→ runs exclusively for the thread holding the lock. Monitor.Exit()→ releases the lock.finallyis important because it guarantees the lock is released even if an exception occurs.
- Mutex
A Mutex also provides synchronized access to a resource in a multithreaded environment. However, the main difference is that synchronization mechanisms such as
lockandMonitorwork only within a single process. In other words, their scope is limited to threads running inside the same process.A Mutex, on the other hand, can provide synchronized access to a resource across multiple processes. This means that threads from different processes can use the same Mutex to coordinate access to a shared resource.
For example, if you have two applications that need synchronized access to a shared resource( a file), you can use a Mutex to ensure that only one application accesses the resource at a time.
Code Exampleusing System; using System.Threading; class Program { static Mutex mutex = new Mutex(); static void Main() { Thread t1 = new Thread(DoWork); Thread t2 = new Thread(DoWork); t1.Start(); t2.Start(); t1.Join(); t2.Join(); } static void DoWork() { Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} is waiting..."); mutex.WaitOne(); // Acquire the mutex try { Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} entered critical section."); // Only one thread can execute this code at a time Thread.Sleep(2000); Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} leaving."); } finally { mutex.ReleaseMutex(); // Release the mutex } } }
How it worksnew Mutex()creates a mutex.mutex.WaitOne()locks/acquires the mutex. If another thread already owns it, the current thread waits.mutex.ReleaseMutex()unlocks/releases it.try/finallyis important so the mutex gets released even if an exception occurs.
- Semaphore
Semaphore is another synchronization mechanism used to control access to a shared resource. Like a Mutex, a named Semaphore can also be used for synchronization across multiple processes. However, unlike a Mutex, a Semaphore can allow multiple threads or processes to access the resource concurrently. It lets you set a limit on the number of threads or processes that can access a critical section at the same time.
Code Exampleusing System; using System.Threading; class Program { // Allow a maximum of 3 threads at the same time static Semaphore semaphore = new Semaphore(3, 3); static void Main() { for (int i = 1; i <= 5; i++) { int threadNumber = i; Thread thread = new Thread(() => DoWork(threadNumber)); thread.Start(); } } static void DoWork(int threadNumber) { Console.WriteLine($"Thread {threadNumber} is waiting..."); semaphore.WaitOne(); // Enter semaphore try { Console.WriteLine($"Thread {threadNumber} entered."); Thread.Sleep(3000); Console.WriteLine($"Thread {threadNumber} is leaving."); } finally { semaphore.Release(); // Leave semaphore } } } - Join
Thread.Join()is a thread synchronization mechanism used to make one thread wait until another thread has completed its execution. It is different fromlock,Mutex, andSemaphorebecauseJoin()does not control access to a shared resource. Instead, it synchronizes the completion/order of threads.using System; using System.Threading; class Program { static void Main() { Thread t1 = new Thread(() => DoWork("Thread 1")); Thread t2 = new Thread(() => DoWork("Thread 2")); Thread t3 = new Thread(() => DoWork("Thread 3")); t1.Start(); t2.Start(); t3.Start(); // Wait for all threads to finish t1.Join(); t2.Join(); t3.Join(); Console.WriteLine("All threads have completed."); } static void DoWork(string name) { Console.WriteLine($"{name} started."); Thread.Sleep(2000); Console.WriteLine($"{name} finished."); } }The important point is that the three threads can run concurrently, but the main thread won't print "All threads have completed." until all three have finished.
| Mechanism | What does it do? |
|---|---|
lock | Provides exclusive access to a critical section |
Monitor | Provides locking and thread coordination |
Mutex | Provides exclusive access, including across processes |
Semaphore | Limits the number of threads/processes accessing a resource |
Thread.Join() | Waits for a thread to complete |
Summary
Thread synchronization is a technique used to coordinate multiple threads when they access a shared resource. Its main goal is to prevent problems such as race conditions and ensure thread safety.
Thanks