HomeNikola Knezevic

In this article

Banner

Getting Started with Concurrency Control in .NET

18 Dec 2025
5 min

Sponsor Newsletter

Concurrency issues can silently break even the most well designed applications.

Without proper synchronization, multiple threads accessing shared resources can cause race conditions, deadlocks and other bugs that are often difficult to detect and reproduce.

That’s where concurrency control comes in ensuring that shared data is accessed safely and predictably.

Concurrency Control

Concurrency control is all about coordinating access to shared data.

In .NET, we have several built-in primitives to help you achieve this:

  • lock
  • Monitor
  • Mutex
  • Semaphore
  • SemaphoreSlim

Each of these has its own strengths. Some are lightweight, some support asynchronous operations and others can work across processes.

Let’s look at some of the most commonly used examples.

Lock Statement

The simplest and most commonly used concurrency primitive in .NET is the lock statement:

csharp
private readonly object _syncLock = new();

public void Increment()
{
    lock (_syncLock)
    {
        _counter++;
    }
}

This ensures that only one thread can execute a given block of code at a time, protecting access to shared data.

Under the hood, the lock statement is syntactic sugar for Monitor.Enter and Monitor.Exit wrapped in a try/finally block.

It’s efficient and perfect for short, synchronous operations.

Lock Type

In the examples above, we used an object to lock on.

While lock can technically be used on any reference type, best practice is to use a private readonly object.

However, with .NET 9 release we got new Lock type, which is more expressive:

csharp
private readonly Lock _syncLock = new();

public void Increment()
{
    lock (_syncLock)
    {
        _counter++;
    }
}

When compiled, this translates to Lock.EnterScope() under the hood, and the scope is disposed automatically when leaving the block.

Semaphore

A Semaphore is a more advanced synchronization primitive.

Unlike lock, which allows only one thread to enter a critical section at a time, a semaphore allows a fixed number of threads to proceed concurrently:

csharp
private static readonly Semaphore _semaphore = new(3, 3);

public void AccessResource()
{
    _semaphore.WaitOne();
    try
    {
        // Up to 3 threads can be here at the same time
        DoWork();
    }
    finally
    {
        _semaphore.Release();
    }
}

In this example, up to three threads can access the resource concurrently.

This is useful when you want to throttle access to limited resources.

SemaphoreSlim

SemaphoreSlim is a lightweight, faster alternative to Semaphore.

It doesn’t rely on kernel handles and most importantly it supports asynchronous operations:

csharp
private static readonly SemaphoreSlim _semaphore = new(3);

public async Task AccessResourceAsync()
{
    await _semaphore.WaitAsync();
    try
    {
        await DoWorkAsync();
    }
    finally
    {
        _semaphore.Release();
    }
}

It’s efficient, simple and designed for high-performance scenarios where async code dominates.

Conclusion

Thread safety has evolved significantly in .NET. From the classic lock statement to the modern Lock type and async-friendly SemaphoreSlim.

Each primitive has its own strengths, everything from simple lock statements to Semaphore and SemaphoreSlim for limited access and async operations.

To learn more about synchronization primitives checkout the official overview.

If you want to check out examples I created, you can find the source code here:

Source Code

I hope you enjoyed it, subscribe and get a notification when a new blog is up!

Subscribe

Stay tuned for valuable insights every Thursday morning.