C# Thread Synchronization

Last Updated : 20 Apr, 2026

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:

C#
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.

C#
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().

C#
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.

C#
class Demo
{
    private static Mutex mutex = new Mutex();

    static void PrintNumbers()
    {
        mutex.WaitOne();
        try{
        for (int i = 1; i <= 3; i++)
        {
            Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} -> {i}");
            Thread.Sleep(200);
        }}
        finally{
        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.

C#
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.

C#
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 does not support inter-process synchronization, unlike Semaphore.

Comment
Article Tags:

Explore