Introduction To Locking And Concurrency Control in .NET 6

Introduction To Locking And Concurrency Control in .NET 6

3 min read ·

Thank you to our sponsors who keep this newsletter free to the reader:

This week's issue is sponsored by QuadSpinner.

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. Instead, I will focus on application-level locking mechanisms.

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 1, 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: 1,
      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: 1,
      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:

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.


Whenever you're ready, there are 4 ways I can help you:

  1. (COMING SOON) REST APIs in ASP.NET Core: You will learn how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API. Join the waitlist!
  2. Pragmatic Clean Architecture: Join 3,600+ students in this comprehensive course that 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.
  3. Modular Monolith Architecture: Join 1,600+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  4. Patreon Community: Join a community of 1,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.
  5. Promote yourself to 60,000+ subscribers by sponsoring this newsletter.

Become a Better .NET Software Engineer

Join 60,000+ engineers who are improving their skills every Saturday morning.