Async Waiting inside C# Locks

Have you ever tried to await a task inside a lock() block? In C#, this statement is invalid:

lock (lockObject)  
    await Task.Delay(1000);

The lock keyword can only be used to synchronize synchronous code. From MSDN:

An await expression cannot occur in the body of a synchronous function, in a query expression, in the block of a lock statement, or in an unsafe context.

Since the introduction of C# 5, async/await is used pretty much everywhere. And why not? The compiler does the difficult work that the developer used to do, and the application retains a logical structure that resembles synchronous code. As a result, you get all the advantages of asynchronous programming with a fraction of the effort.

The problem is that the need to synchronize asynchronous code blocks is coming up quite often. Eric Lippert1 notes that the reason that this is not implemented by the compiler team is not because it's difficult to implement, but rather to protect the developer from making mistakes2; awaiting inside a lock is a recipe for producing deadlocks.

Introducing the Mutex and the Semaphore

In simple terms, a semaphore is a data type that is used for synchronizing access from multiple threads. Semaphores are a useful tool in the prevention of race conditions. There are two types of semaphores:

  1. Counting Semaphores: As the name implies, counting semaphores allow a number of simultaneous threads to access a shared resource (up to a maximum number you specify). When threads request access to the resource, the semaphore count decrements and when they release it, it increments back again.
  2. Binary Semaphores or Mutex: A Mutex is essentially a semaphore with a value of 1. A Mutex cannot be released to more than one thread at the same time; it provides mutual exclusion (hence the name). As long as someone has the mutex, the others must wait.

.NET Semaphore and SemaphoreSlim

The System.Threading.Semaphore class is a wrapper around the Win32 semaphore object (counting semaphores). This is a system wide semaphore, so it can be used between multiple processes.

On the other hand, the System.Threading.SemaphoreSlim is a lightweight, fast semaphore that is provided by the CLR and used for waiting within a single process when wait times are expected to be very short.

Replacing the Lock with a Semaphore

So now, that we know what a Semaphore is we may go ahead and replace the lock with a Semaphore. In our case, the SemaphoreSlim class is the ideal data type to use since we will be using it in a single process.

It is vital to always release the Semaphore when you are ready, this is why it is suggested to be placed inside a try...finally clause3.

Calling WaitAsync on the semaphore produces a task that will be completed when that thread has been granted access to the Semaphore.

//Instantiate a Singleton of the Semaphore with a value of 1. This means that only 1 thread can be granted access at a time.
static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1,1);  
//Asynchronously wait to enter the Semaphore. If no-one has been granted access to the Semaphore, code execution will proceed, otherwise this thread waits here until the semaphore is released 
await semaphoreSlim.WaitAsync();  
    await Task.Delay(1000);
    //When the task is ready, release the semaphore. It is vital to ALWAYS release the semaphore when we are ready, or else we will end up with a Semaphore that is forever locked.
    //This is why it is important to do the Release within a try...finally clause; program execution may crash or take a different path, this way you are guaranteed execution
  1. Eric Lippert (@ericlippert) currently designs programming languages at Facebook, but he was previously working on the C# compiler at Microsoft. He is also a C# MVP.


  3. try-finally (C# Reference)