C# Synchronization

Introduction

Synchronization in C# is crucial when dealing with multithreaded applications to ensure that multiple threads can access shared resources without causing data corruption or inconsistency. C# provides several synchronization primitives to manage access to shared resources, including locks, monitors, mutexes, semaphores, and more. These primitives help you coordinate the actions of multiple threads and ensure thread safety.

Key Synchronization Primitives

  1. lock Statement
  2. Monitor Class
  3. Mutex
  4. Semaphore
  5. AutoResetEvent and ManualResetEvent
  6. ReaderWriterLockSlim

1. lock Statement

The lock statement is the simplest and most commonly used synchronization mechanism. It ensures that a block of code runs to completion without being interrupted by other threads.

Example

using System;
using System.Threading;

namespace LockExample
{
    class Program
    {
        private static readonly object _lock = new object();
        private static int _counter = 0;

        static void Main(string[] args)
        {
            Thread thread1 = new Thread(IncrementCounter);
            Thread thread2 = new Thread(IncrementCounter);

            thread1.Start();
            thread2.Start();

            thread1.Join();
            thread2.Join();

            Console.WriteLine($"Final counter value: {_counter}");
        }

        static void IncrementCounter()
        {
            for (int i = 0; i < 1000; i++)
            {
                lock (_lock)
                {
                    _counter++;
                }
            }
        }
    }
}

Output

Final counter value: 2000

2. Monitor Class

The Monitor class provides a more flexible way to lock a section of code. It offers methods like Enter, Exit, Wait, and Pulse to control the synchronization.

Example

using System;
using System.Threading;

namespace MonitorExample
{
    class Program
    {
        private static readonly object _lock = new object();
        private static int _counter = 0;

        static void Main(string[] args)
        {
            Thread thread1 = new Thread(IncrementCounter);
            Thread thread2 = new Thread(IncrementCounter);

            thread1.Start();
            thread2.Start();

            thread1.Join();
            thread2.Join();

            Console.WriteLine($"Final counter value: {_counter}");
        }

        static void IncrementCounter()
        {
            for (int i = 0; i < 1000; i++)
            {
                Monitor.Enter(_lock);
                try
                {
                    _counter++;
                }
                finally
                {
                    Monitor.Exit(_lock);
                }
            }
        }
    }
}

Output

Final counter value: 2000

3. Mutex

A Mutex is a synchronization primitive that can be used to manage access to a resource across multiple threads and even processes. Unlike lock, which is limited to the current application domain, a Mutex can be used system-wide.

Example

using System;
using System.Threading;

namespace MutexExample
{
    class Program
    {
        private static readonly Mutex _mutex = new Mutex();
        private static int _counter = 0;

        static void Main(string[] args)
        {
            Thread thread1 = new Thread(IncrementCounter);
            Thread thread2 = new Thread(IncrementCounter);

            thread1.Start();
            thread2.Start();

            thread1.Join();
            thread2.Join();

            Console.WriteLine($"Final counter value: {_counter}");
        }

        static void IncrementCounter()
        {
            for (int i = 0; i < 1000; i++)
            {
                _mutex.WaitOne(); // Acquire the mutex
                try
                {
                    _counter++;
                }
                finally
                {
                    _mutex.ReleaseMutex(); // Release the mutex
                }
            }
        }
    }
}

Output

Final counter value: 2000

4. Semaphore

A Semaphore controls access to a resource pool, allowing a specified number of threads to enter the critical section simultaneously. It is useful for limiting the number of concurrent accesses to a resource.

Example

using System;
using System.Threading;

namespace SemaphoreExample
{
    class Program
    {
        private static readonly Semaphore _semaphore = new Semaphore(2, 2);
        private static int _counter = 0;

        static void Main(string[] args)
        {
            for (int i = 0; i < 5; i++)
            {
                Thread thread = new Thread(IncrementCounter);
                thread.Start();
            }
        }

        static void IncrementCounter()
        {
            _semaphore.WaitOne(); // Acquire the semaphore

            try
            {
                for (int i = 0; i < 1000; i++)
                {
                    _counter++;
                }

                Console.WriteLine($"Counter value: {_counter}");
            }
            finally
            {
                _semaphore.Release(); // Release the semaphore
            }
        }
    }
}

Output

Counter value: 1000
Counter value: 2000
Counter value: 3000
Counter value: 4000
Counter value: 5000

5. AutoResetEvent and ManualResetEvent

AutoResetEvent and ManualResetEvent are signaling mechanisms to control thread execution. They are used to signal one or more waiting threads that an event has occurred.

Example

using System;
using System.Threading;

namespace EventExample
{
    class Program
    {
        private static readonly AutoResetEvent _autoResetEvent = new AutoResetEvent(false);

        static void Main(string[] args)
        {
            Thread workerThread = new Thread(DoWork);
            workerThread.Start();

            Console.WriteLine("Main thread waiting for worker thread to signal...");
            _autoResetEvent.WaitOne(); // Wait for signal
            Console.WriteLine("Main thread received signal. Continuing...");
        }

        static void DoWork()
        {
            Thread.Sleep(2000); // Simulate work
            Console.WriteLine("Worker thread signaling main thread...");
            _autoResetEvent.Set(); // Signal
        }
    }
}

Output

Main thread waiting for worker thread to signal...
Worker thread signaling main thread...
Main thread received signal. Continuing...

6. ReaderWriterLockSlim

ReaderWriterLockSlim is a synchronization primitive that allows multiple threads to read a resource simultaneously but only one thread to write to it at a time. It is useful for scenarios where reads are much more frequent than writes.

Example

using System;
using System.Threading;

namespace ReaderWriterLockSlimExample
{
    class Program
    {
        private static readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
        private static int _counter = 0;

        static void Main(string[] args)
        {
            Thread writerThread = new Thread(Write);
            Thread readerThread1 = new Thread(Read);
            Thread readerThread2 = new Thread(Read);

            writerThread.Start();
            readerThread1.Start();
            readerThread2.Start();

            writerThread.Join();
            readerThread1.Join();
            readerThread2.Join();
        }

        static void Write()
        {
            _rwLock.EnterWriteLock();
            try
            {
                _counter++;
                Console.WriteLine($"Counter incremented to {_counter}");
            }
            finally
            {
                _rwLock.ExitWriteLock();
            }
        }

        static void Read()
        {
            _rwLock.EnterReadLock();
            try
            {
                Console.WriteLine($"Counter value read as {_counter}");
            }
            finally
            {
                _rwLock.ExitReadLock();
            }
        }
    }
}

Output

Counter incremented to 1
Counter value read as 1
Counter value read as 1

Conclusion

Synchronization in C# is essential for ensuring that multiple threads can access shared resources without causing data corruption or inconsistency. The lock statement, Monitor class, Mutex, Semaphore, AutoResetEvent, ManualResetEvent, and ReaderWriterLockSlim are powerful synchronization primitives that help you manage thread synchronization effectively. By understanding and using these synchronization mechanisms, you can write more reliable and thread-safe multithreaded applications in C#.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top