Search code examples
.netmultithreadingasynchronouslockingsemaphore

What is the Correct Usage of SempahoreSlim as a Lock in Async Code?


It seems like in async code these days, SemaphoreSlim is the recommended replacement for lock(obj) {}. I found this recommendation for how to use it: https://blog.cdemi.io/async-waiting-inside-c-sharp-locks/

In particular, this person suggest this code:

//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();
}

To me it seems like this code violates the advice I used to see about how to lock, which is to keep in mind that your code could be interrupted at any time and code for that. If any exception is thrown just after await sempahoreSlim.WaitAsync() and before the try statement is entered, the semaphore will never be released. This kind of problem is exactly why I thought the lock statement and using statements were introduced with such great results.

Is there a reference somewhere that definitively explains that this code is valid? Perhaps try/finally statements are actually entered before the code can be interrupted, which is something I've never known about before? Or, is there a different pattern that would actually be the correct use of a semaphore as a lock, or some other locking mechanism for async .NET code?


Solution

  • Yes, it is true that in theory something could happen between the await semaphoreSlim.WaitAsync(); and the try, but in reality: in that scenario your app is already toast, as it is in the process of imploding call-stacks. In reality, while this is a theoretical concern, there isn't much that you can usefully do anyway, and your process is about to be put down ungracefully like the sickly thing that it is.

    So my advice: don't worry too much about it :)

    (in reality, the much bigger risk is a thread-pool spiral of death, usually caused by sync-over-async on thread-pool threads, meaning that even though you have acquired the semaphore semantically, there is no pool thread to give to you to actually do the thing, allowing you to get back to releasing it)