So, a pattern I use very often while working on my UWP app is to use a SemaphoreSlim
instance to avoid race conditions (I prefer not to use lock
as it needs an additional target object, and it doesn't lock asynchronously).
A typical snippet would look like this:
private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1);
public async Task FooAsync()
{
await Semaphore.WaitAsync();
// Do stuff here
Semaphore.Release();
}
With the additional try/finally
block around the whole thing, if the code in between could crash but I want to keep the semaphore working properly.
To reduce the boilerplate, I tried to write a wrapper class that would have the same behavior (including the try/finally
bit) with less code needed. I also didn't want to use a delegate
, as that'd 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 the way it works is that by using it you only need the following:
private readonly AsyncMutex Mutex = new AsyncMutex();
public async Task FooAsync()
{
using (_ = await Mutex.Lock())
{
// Do stuff here
}
}
One line shorter, and with try/finally
built in (using
block), awesome.
Now, I have no idea why this works, despite the discard operator being used.
That discard _ was actually just out of curiosity, as I knew I should have just written var _
, since I needed that IDisposable
object to be used at the end of the using
block, and 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 the field V_1
and correctly disposed.
So, why does this happen? The docs don't say anything about the discard operator being used with the using
block, and they just say the discard assignment is ignored completely.
Thanks!
The using
statement does not require an explicit declaration of a local variable. An expression is also allowed.
The language specification specifies the following syntax.
using_statement : 'using' '(' resource_acquisition ')' embedded_statement ; resource_acquisition : local_variable_declaration | expression ;
If the form of resource_acquisition is local_variable_declaration then the type of the local_variable_declaration must be either dynamic or a type that can be implicitly converted to
System.IDisposable
. If the form of resource_acquisition is expression then this expression must be implicitly convertible toSystem.IDisposable
.
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#the-using-statement
An assignment of an existing variable (or discarding the result) is also an expression. For example the following code compiles:
var a = (_ = 10);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With