Introduction To Locking And Concurrency Control in .NET 6

Introduction To Locking And Concurrency Control in .NET 6

In this week's newsletter, we'll see how we can work with locking in .NET 6.

We won't talk about how the lock is actually implemented at the operating system level. We will focus on application-level locking mechanisms instead.

Locking allows us to control how many threads can access some piece of code. Why would you want to do this?

Usually because you want to protect access to expensive resources, and you need the concurrency control that locking enables.

We will use a simple BankAccount class with a Deposit method to illustrate how to implement locking.

The C# Lock Statement

The C# language supports locking with the lock statement. You can use the lock statement to define a code block that only one thread can access.

The lock statement acquires a mutual-exclusion lock (mutex) for a given object, executes the statement block, and releases the lock.

lock(_lock)
{
   // Your code...
}

Here _lock is a reference type, usually an object instance.

Let's see how we can implement the BankAccount class using the lock statement:

public class BankAccount
{
   private static readonly object _lock = new();
   private decimal _balance;

   public void Deposit(decimal amount)
   {
      lock(_lock)
      {
         _balance += amount;
      }
   }
}

The first thread to reach and execute the lock statement will be allowed to update the _balance. Any other threads will block until the lock is released.

Locking With Semaphore

The Semaphore class is another option we can use to achieve the same effect.

We'll use the Semaphore constructor to set the initialCount to 0, which means that the Semaphore is open at the start. And we will also set the maximumCount to 1, which means that only one thread is allowed to enter the Semaphore.

Let's see how we can implement the BankAccount class using the Semaphore:

public class BankAccount
{
   private static readonly Semaphore _semaphore = new(
      initialCount: 0,
      maximumCount: 1);

   private decimal _balance;

   public void Deposit(decimal amount)
   {
      _semaphore.WaitOne();

      _balance += amount;

      _semaphore.Release();
   }
}

To enter the Semaphore, we have to call the WaitOne method.

If no thread was previously inside, our thread is allowed to enter the Semaphore and update the balance.

After updating the balance, we call the Release method to release the Semaphore for other threads that might be waiting.

Asynchronous Locking With SemaphoreSlim

What if we wanted to call an asynchronous method in a locked context?

We can't use the lock statement as it doesn't support asynchronous calls. Awaiting an asynchronous call inside a lock statement will cause a compilation error.

The Semaphore class can solve this problem.

But I want to show you another option that we have, SemaphoreSlim. It's a lightweight alternative to the Semaphore class and has async methods.

Let's see how we can implement the BankAccount class using SemaphoreSlim:

public class BankAccount
{
   private static readonly SemaphoreSlim _semaphore = new(
      initialCount: 0,
      maximumCount: 1);

   private decimal _balance;

   public async Task Deposit(decimal amount)
   {
      await _semaphore.WaitAsync();

      _balance += amount;

      _semaphore.Release();
   }
}

Notice that I updated the Deposit method to return a Task.

This time, we're calling WaitAsync to block the current thread until it can enter the semaphore.

After updating the balance, we call the Release method to release the SemaphoreSlim like in the previous example.

Are There Other Options For Locking in .NET?

So far I mentioned three options to implement locking:

  • lock statement

  • Semaphore

  • SemaphoreSlim

However, .NET has other classes for concurrency control that you can explore like Monitor, Mutex, ReaderWriterLock and many more.

I hope you enjoyed this brief introduction to a very complex topic.


P.S. Whenever you’re ready, there are 2 ways I can help you:

  1. Pragmatic Clean Architecture: This comprehensive course will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture. Join 950+ students here.

  2. Patreon Community: Think like a senior software engineer with access to the source code I use in my YouTube videos and exclusive discounts for my courses. Join 820+ engineers here.