Thread Synchronization in Multithreading

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-1

If 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-2
Suppose 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.

  1. 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 example
    
    class 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 Increment() simultaneously, the lock ensures that the count++ operation is performed by one thread at a time.

    Example with multiple threads
    
    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 lock, you might expect 200000, but due to a race condition, the result can be less than 200000.With lock, access to count++ is synchronized, so the result will be 200000

    How it works
    • A lock object is created lockObject
    • A thread reaches the lock statement and tries to acquire the lock
      
      lock (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: lock usually create a private object specifically for locking, avoid locking on publicly accessible objects such as this, typeof(MyClass), or strings.

  2. 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.

    Example
    
    using 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 works
    • Monitor.Enter() → acquires the lock.
    • The code inside try → runs exclusively for the thread holding the lock.
    • Monitor.Exit() → releases the lock.
    • finally is important because it guarantees the lock is released even if an exception occurs.
  3. Mutex

    A Mutex also provides synchronized access to a resource in a multithreaded environment. However, the main difference is that synchronization mechanisms such as lock and Monitor work 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 Example
    
    using 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 works
    • new 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/finally is important so the mutex gets released even if an exception occurs.
  4. 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 Example
    
    using 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
        }
      }
    }
    
  5. Join

    Thread.Join() is a thread synchronization mechanism used to make one thread wait until another thread has completed its execution. It is different from lock, Mutex, and Semaphore because Join() 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.


All Synchronization Mechanisms

MechanismWhat does it do?
lockProvides exclusive access to a critical section
MonitorProvides locking and thread coordination
MutexProvides exclusive access, including across processes
SemaphoreLimits 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

Kailash Chandra Behera

I am an IT professional with over 12 years of experience in the full software development life cycle for Windows, services, and web-based applications using Microsoft .NET technologies.

Previous Post Next Post

نموذج الاتصال