Agenda

Introduction to synchronization problems

When does the synchronization problem happen?

Critical Section

Race Condition

Preemption

Solutions to the synchronization problem

In C#, there are several ways to synchronize access to shared resources to ensure thread safety and prevent race conditions.

Using lock keyword

The lock keyword provides a convenient way to create a synchronized block of code. It internally uses the Monitor class to achieve synchronization. The lock keyword ensures that only one thread can execute the locked code block at a time.

Example

C#
private static object syncObject = new object();
private void Increment()
{
    lock(syncObject)
    {
        // Critical section: Access shared resource
    }
}

Using Monitor Class

Instead of using the lock keyword, we can directly use methods of the Monitor class for synchronization.

Example

C#
private static object syncObject = new object();
private void Increment()
{
    Monitor.Enter(syncObject);
    try
    {
        // Critical section: Access shared resource
    }
    finally
    {
        Monitor.Exit(syncObject);
    }
}

Using Mutex

A mutex is a synchronization primitive that allows only one thread to acquire it at a time. It's typically used for inter-process synchronization to synchronize threads within the same process.

Example

C#
private static Mutex mutex = new Mutex();
private void Increment() 
{
    mutex.WaitOne();
    try
    {
        // Critical section: Access shared resource
    }
    finally 
    {
        mutex.ReleaseMutex();
    }
}

Using Semaphore

A semaphore is a synchronization primitive that allows a specified number of threads to enter a critical section simultaneously. It's useful when we want to limit the number of threads accessing a resource.

Example

C#
private static Semaphore semaphore = new Semaphore(1, 1); // Limits access to one thread
private void Increment()
{
    semaphore.WaitOne();
    try
    {
        // Critical section: Access shared resource
    }
    finally
    {
        semaphore.Release();
    }
}

Using Interlocked Class

The Interlocked class provides atomic operations for variables that are shared between threads. It's useful for performing simple operations like incrementing a counter without the need for locking.

Example

C#
private int counter = 0;
public void Increment()
{
    Interlocked.Increment(ref counter);
}

Properties of a good solution to the synchronization problem

  1. Mutual Exclusion: Only one thread should be allowed inside the critical section at any point in time.
  2. Progress: Overall system should keep on making progress. There shouldn't be a deadlock condition.
  3. Bounded waiting: No thread should wait outside the critical section infinitely. There should be some bound on the waiting time.
  4. No Busy Waiting: If a thread has to continuously check if they can enter inside the critical section or not is Busy Waiting.
    • while(!allowed to enter critical section)
      C#
      {
          checking(); // <---- This is the busy waiting.
      }
    • there shouldn't be Busy waiting as it can have several consequences like
      • Inefficient use of CPU resources and wasted energy.
      • Reduced performance.
      • Increased power consumption.
      • Potential deadlocks and etc.
  5. The ideal solution should have some kind of Notification system. Once the thread is done with their task in the critical section, they will notify all the threads waiting outside.