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[2]; 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:
- 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.
- 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[3].
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();
}
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. ↩︎