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 Lippert[1] 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 mistakes[1:1]; 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 clause[1:2].

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();
try
{
    await Task.Delay(1000);
}
finally
{
    //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
    semaphoreSlim.Release();
}

  1. try-finally (C# Reference) ↩︎ ↩︎ ↩︎