C# Thread Synchronization

Synchronization (in programming) refers to the coordination of multiple threads or processes to ensure that they access shared resources in a safe and orderly manner, preventing conflicts or inconsistencies.

synchronization helps manage access to shared data in a multithreaded environment to avoid issues like race conditions or data corruption.

There are several mechanisms provided by the language and the .NET framework for synchronization, such as locks, mutexes, and semaphores.

1. Lock (Monitor / lock keyword)

A lock ensures that only one thread can access a specific block of code at a time. This prevents multiple threads from accessing a shared resource simultaneously, ensuring thread safety.

Example:


using System;
using System.Threading;

class MyProgram
{
    private static readonly object _lockObject = new object();

    static void Main()
    {
        // Simulate threads accessing the same resource
        Thread t1 = new Thread(Print);
        Thread t2 = new Thread(Print);

        t1.Start();
        t2.Start();
    }

    static void Print()
    {
        lock (_lockObject)  // Only one thread can print at a time
        {
            Console.WriteLine("Printing data...");
            Thread.Sleep(1000); // Simulate time to print
            Console.WriteLine("Printed data");
        }
    }
}

Explanation:

The lock (_lockObject) ensures that only one thread can execute the Print method at a time. If one thread is already printing, the other will wait until the first one finishes.

2. Mutex

A mutex is a synchronization object used to control access to a resource across multiple processes, not just threads within a single process. A mutex allows only one thread or process to access the resource at a time.

Example:


using System;
using System.Threading;

class MyProgram
{
    static Mutex mutex = new Mutex();

    static void Main()
    {
        Thread t1 = new Thread(AccessResource);
        Thread t2 = new Thread(AccessResource);

        t1.Start();
        t2.Start();
    }

    static void AccessResource()
    {
        mutex.WaitOne();  // Acquire the mutex (lock the resource)
        Console.WriteLine("Resource being accessed...");
        Thread.Sleep(1000); // Simulate time accessing resource
        Console.WriteLine("Resource released.");
        mutex.ReleaseMutex();  // Release the mutex (unlock the resource)
    }
}

Explanation:

mutex.WaitOne() acquires the mutex (locks the resource). If another thread or process already holds it, the current thread waits.

mutex.ReleaseMutex() releases the mutex (unlocks the resource), allowing others to access it.

3. Semaphore

A semaphore controls access to a resource, but unlike a lock, it allows multiple threads to access the resource at the same time, up to a specified limit. It’s useful when you want to limit the number of threads accessing a resource.

Example:


using System;
using System.Threading;

class Program
{
    static Semaphore semaphore = new Semaphore(2, 2);  // Allow up to 2 threads

    static void Main()
    {
        Thread t1 = new Thread(ParkCar);
        Thread t2 = new Thread(ParkCar);
        Thread t3 = new Thread(ParkCar);
        
        t1.Start();
        t2.Start();
        t3.Start();
    }

    static void ParkCar()
    {
        semaphore.WaitOne();  // Acquire the semaphore (reserve parking)
        Console.WriteLine("Car parked.");
        Thread.Sleep(1000); // Simulate parking time
        Console.WriteLine("Car leaving.");
        semaphore.Release();  // Release the semaphore (free parking space)
    }
}

Explanation:

semaphore.WaitOne() acquires the semaphore, which reduces the available spaces. If all spaces are filled, other threads must wait.

semaphore.Release() releases the semaphore, freeing up a space for another thread.