So, the template that I use very often when working on my UWP application is to use an instance of SemaphoreSlim to avoid race conditions (I prefer not to use lock , because this requires an additional target, t block asynchronously).
A typical snippet would look like this:
private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1); public async Task FooAsync() { await Semaphore.WaitAsync();
With an extra try/finally block around everything, if the code between them could fail, but I want the semaphore to work correctly.
To reduce the pattern, I tried to write a wrapper class that would have the same behavior (including the try/finally bit) with less code needed. I also did not want to use delegate , as this would create an object every time, and I just wanted to reduce my code without changing the way it worked.
I came up with this class (comments removed for brevity):
public sealed class AsyncMutex { private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1); public async Task<IDisposable> Lock() { await Semaphore.WaitAsync().ConfigureAwait(false); return new _Lock(Semaphore); } private sealed class _Lock : IDisposable { private readonly SemaphoreSlim Semaphore; public _Lock(SemaphoreSlim semaphore) => Semaphore = semaphore; void IDisposable.Dispose() => Semaphore.Release(); } }
And how it works is that using it you only need the following:
private readonly AsyncMutex Mutex = new AsyncMutex(); public async Task FooAsync() { using (_ = await Mutex.Lock()) {
One line is shorter and with try/finally built-in ( using block) is awesome.
Now I have no idea why this works, even though the reset operator b.
This drop of _ is really just out of curiosity, because I knew that I just had to write var _ , since I needed an IDisposable object that would be used at the end of the using block, not discarder.
But, to my surprise, the same IL is generated for both methods:
.method public hidebysig instance void T1() cil managed { .maxstack 1 .locals init ( [0] class System.Threading.Tasks.AsyncMutex mutex, [1] class System.IDisposable V_1 ) IL_0001: newobj instance void System.Threading.Tasks.AsyncMutex::.ctor() IL_0006: stloc.0 // mutex IL_0007: ldloc.0 // mutex IL_0008: callvirt instance class System.Threading.Tasks.Task`1<class System.IDisposable> System.Threading.Tasks.AsyncMutex::Lock() IL_000d: callvirt instance !0/*class System.IDisposable*/ class System.Threading.Tasks.Task`1<class System.IDisposable>::get_Result() IL_0012: stloc.1 // V_1 .try { // Do stuff here.. IL_0025: leave.s IL_0032 } finally { IL_0027: ldloc.1 // V_1 IL_0028: brfalse.s IL_0031 IL_002a: ldloc.1 // V_1 IL_002b: callvirt instance void System.IDisposable::Dispose() IL_0031: endfinally } IL_0032: ret }
The "discarder" IDisposable is stored in field V_1 and is placed correctly.
So why is this happening? docs says nothing about the reset statement that is used with the using block, and they just say that the reset destination is completely ignored.
Thanks!