Locking (Synchronization) in C#

Locking (Synchronization) in C#

Concurrency and multithreading are powerful features in modern programming, but they bring challenges, especially in managing shared resources. C# offers various synchronization primitives to prevent race conditions and ensure thread-safe operations. In this article, we’ll explore the different types of locking mechanisms available in C#, their usage scenarios, and code examples.

Types of locking

It is essential to know why so many synchronization objects exist and what are their usages. We can categorize locking capabilities in C# in different ways. They can be categorized by the exclusiveness or based on single process or multi process locking capability.

Exclusiveness:

  • Exclusive locking: They allow only one thread to perform changes on shared objects. lock, Mutex and SpinLock belong to this category.
  • Non-exclosive locking: These synchronization objects limit access to shared objects. That means maybe two threads can simultaneously access a shared object for example. Semaphore, ReaderWriterLock and ReaderWriterLockSlim belong to this category.
  • Signaling: They block a thread (on some threads) until they receive notification(s) or signal(s) from another thread(s). ManualResetEvent, ManualResetEventSlim, AutoResetEvent, CountdownEvent and Barrier belong to this category.

In this article I won’t go through signals because I covered them in “Signaling in C#”.

Single/Multi process support:

  • Single process support: These synchronization objects can be used across threads in a single process. lock, SpinLock, ReaderWriterLock and ReaderWriterLockSlim are in this category.
  • Multi process support: These objects can be used across threads or processes or programs in a computer. Mutex and Semaphore are in this category.

lock Statement

The lock statement is a high-level synchronization primitive.

It prevents multiple threads from accessing a critical section simultaneously by acquiring an exclusive lock on an object.

Use lock when you need simple mutual exclusion for a block of code.

Note that it can lead to deadlocks if not used carefully.

private static readonly object _logLock = new object();

public void LogMessage(string message)
{
     lock (_logLock)
     {
          File.AppendAllText("log.txt", message + Environment.NewLine);
     }
}        

Monitor Class

Monitor provides greater flexibility than lock, such as enabling threads to wait for a condition to be signaled. lock is actually based on Monitor. Monitor has methods like Enter an Exit to specify the block of code that you like to include for locking.

private static Mutex _mutex = new Mutex();
public void AccessResource()
{
    if (_mutex.WaitOne())
    {
        try
        {
            Console.WriteLine("Mutex lock acquired.");
        }
        finally
        {
            _mutex.ReleaseMutex();
        }
    }
}        

Here is a producer-consumer queue using Monitor:

private Queue<int> _queue = new Queue<int>();
private readonly object _queueLock = new object();

public void Produce(int item)
{
     lock (_queueLock)
     {
          _queue.Enqueue(item);
          Monitor.Pulse(_queueLock); // Notify waiting consumers
     }
}

public void Consume()
{
     lock (_queueLock)
     {
          while (_queue.Count == 0)
          {
              Monitor.Wait(_queueLock); // Wait for items
          }
          int item = _queue.Dequeue();
          Console.WriteLine($"Consumed: {item}");
     }
}        

Mutex

A Mutex is a cross-process synchronization primitive, meaning it can synchronize threads across multiple applications. The following example shows how we can use Mutex to avoid execution of multiple instance of a program:

static void Main()
{
     using (var mutex = new Mutex(false, "MyAppMutex"))
     {
          if (!mutex.WaitOne(0, false))
          {
              Console.WriteLine("Application is already running.");
              return;
          }

          Console.WriteLine("Application started.");
          Console.ReadLine();
     }
}        

Semaphore and SemaphoreSlim

A Semaphore restricts the number of threads accessing a resource. SemaphoreSlim is a lighter, in-process version.

Semaphore is like a petrol station which has limited capacity and if it is full, no more cars can enter and if one car exits, then one car can enter.

Semaphore has a capacity which limits the number of threads that can access a shared resource. If we reduce the capacity to one, then it works like a Mutex.

Semaphore doesn’t have “Owner” so any thread can call Release method. It is called “thread agnostic”.

The following example limits the number of simultaneous connections to a database:

private static SemaphoreSlim _semaphore = new SemaphoreSlim(3); // Max 3 connections

public async Task AccessDatabase()
{
     await _semaphore.WaitAsync();
     try
     {
          Console.WriteLine("Accessing database...");
          await Task.Delay(1000); // Simulate work
     }
     finally
     {
          _semaphore.Release();
     }
}        

ReaderWriterLock and ReaderWriterLockSlim

ReaderWriterLock and ReaderWriterLockSlim allow multiple readers or a single writer at any given time, optimizing for scenarios where reads vastly outnumber writes.

One of the uses of this class is designed to provide better access to a shared resource like a file for both reading and writing for example when the number of read access is much higher than write access.

They have actually two types of lock inside, a write lock which is exclusive and a read lock.

The following example shows how to use ReaderWriteLockSlim to read configuration values that are infrequently updated.

private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private Dictionary<string, string> _config = new Dictionary<string, string>();

public string ReadConfig(string key)
{
     _lock.EnterReadLock();
     try
     {
          return _config[key];
     }
     finally
     {
          _lock.ExitReadLock();
     }
}

public void WriteConfig(string key, string value)
{
     _lock.EnterWriteLock();
     try
     {
          _config[key] = value;
     }
     finally
     {
          _lock.ExitWriteLock();
     }
}        

SpinLock

SpinLock avoids thread context switching by repeatedly checking for lock availability. It’s suitable for very short critical sections.

Look at the example implements a high frequency counter:

private SpinLock _spinLock = new SpinLock();
private int _counter = 0;

public void IncrementCounter()
{
     bool lockTaken = false;
     try
     {
          _spinLock.Enter(ref lockTaken);
          _counter++;
     }
     finally
     {
          if (lockTaken) _spinLock.Exit();
     }
}        

Interlocked Class

Interlocked provides atomic operations, such as incrementing and exchanging values, without explicit locking.

Let’s see the usage in action, an atomic counter:

private int _counter = 0;
public void IncrementCounter()
{
     Interlocked.Increment(ref _counter);
     Console.WriteLine($"Counter: {_counter}");
}        

Conclusion

Choose the locking mechanism based on your specific requirements:

  • Use lock or Monitor for general-purpose thread synchronization.
  • Use Mutex for cross-process synchronization.
  • Use SemaphoreSlim to manage resource pools.
  • Use ReaderWriterLockSlim for read-heavy workloads.
  • Use SpinLock or Interlocked for performance-critical atomic operations.

Understanding these tools allows you to write efficient, thread-safe applications tailored to your needs.

#lock #multithread #monitor #mutex #semaphore #semaphoreslim #readerwriterlock #readerwriterlockslim #spinlock #interlock #csharp #dotnet


Vahid Tavakolpour

Senior Java Developer @ Code Nomads

3 个月

Nice, briefly explained all kind of locks. Thanks for sharing.

Hassan Vaezzadeh

Software Engineer | Backend Engineer | C#.NET | Cloud & Microservices | System Design | Eager to Relocation | #leadership

3 个月

A static lock is shared across all instances of your current class. This means that any operation involving a lock on any instance of your current class will block operations on other instances. This can lead to unnecessary contention and reduced concurrency.

回复

要查看或添加评论,请登录

Amir Doosti的更多文章

  • Network Programming in c# - Part 2 (HTTP programming)

    Network Programming in c# - Part 2 (HTTP programming)

    In the previous article I talked about Socket programming. Today, I’m going to cover another type of network…

  • Network Programming in C# - Part 1

    Network Programming in C# - Part 1

    Network programming in C# involves using .NET libraries to communicate between systems over a network.

    2 条评论
  • Plotting in C# (Part 4 - ScottPlot)

    Plotting in C# (Part 4 - ScottPlot)

    ScottPlot is an open-source, .NET-based charting library designed for creating high-performance, interactive plots in…

  • Plotting in C# (Part 3 - OxyPlot)

    Plotting in C# (Part 3 - OxyPlot)

    OxyPlot is a lightweight, open-source plotting library designed specifically for .NET applications, supporting…

    2 条评论
  • Plotting in C#.Net (Part2 - LiveCharts2)

    Plotting in C#.Net (Part2 - LiveCharts2)

    LiveCharts is a versatile and modern charting library that supports a variety of charts and visualizations with smooth…

  • Plotting in C#.Net (Part 1 - General)

    Plotting in C#.Net (Part 1 - General)

    Plotting is a crucial tool for data analysis, visualization, and communication. There are many reasons why we need to…

    2 条评论
  • Half-Precision floating point in C#

    Half-Precision floating point in C#

    Recently I encountered a problem in a system where we needed to use floating point but we had just two bytes memory for…

    3 条评论
  • Working with Excel files in .Net

    Working with Excel files in .Net

    Using Excel files in software applications is common for several reasons, as they provide a practical and versatile…

  • ReadOnly Collections vs Immutable Collections

    ReadOnly Collections vs Immutable Collections

    In C#, both readonly collections and immutable collections aim to prevent modifications to the collection, but they…

  • Deconstructor in C#

    Deconstructor in C#

    I have often heard developers use deconstructor and destructor interchangeably while these words have completely two…

社区洞察

其他会员也浏览了