Thursday 3 March 2016

Why async/await locks

I recently wrote a piece of asynchronous code using async/await. This piece of code also had to lock a shared resource. This shared resource would be retrieved via await GetSharedResourceAsync();
I got asked why i used a SemaphoreSlim and await WaitAsync(); and not a classic lock(){}

 

For simplicity I'll break down the lock to what it really is: a Monitor.Enter/Monitor.Exit in a try/finally and then get rid of the try finallys' to have small concise examples.


So basically we need to compare:



Monitor.Enter(padlock);
await DoSomethingAsync();
Monitor.Exit(padlock);

 vs 


await _asyncSemaphore.WaitAsync();
await DoSomethingAsync();
_asyncSemaphore.Release();

where _asyncSemaphore is Microsoft's SemaphoreSlim although there are other implementations as well, outside MS.


Well the answer is quite simple really, Monitor.Enter/Monitor.Exit must be called by the same thread (or get a SynchronizationLockException). await DoSomething(), however, makes no guarantees that the returned task will resume in the same thread.

Additionally, if someone up the parent call chain uses ConfigureAwait(false) then the child tasks are explicitly prevented from capturing the SynchronizationContext. If you are absolutely sure that ConfigureAwait(false) is not used and a SynchronizationContext is in place then you could safely use a synchronous lock spanning an async operation.


On the other hand,


await _awaitableSyncPrimitive.WaitAsync();
DoSomething(); //sync
_awaitableSyncPrimitive.Release(); 

is safe, but then again, in that case, so is lock(){}.

No comments:

Post a Comment