This is an attempted improvement of Bill Tarbell's LockSync
extension method for the SemaphoreSlim
class. By using a value-type IDisposable
wrapper and a ValueTask
return type, it is possible to reduce significantly the additional allocations beyond what the SemaphoreSlim
class allocates by itself.
public static ReleaseToken Lock(this SemaphoreSlim semaphore,
CancellationToken cancellationToken = default)
{
semaphore.Wait(cancellationToken);
return new ReleaseToken(semaphore);
}
public static async ValueTask<ReleaseToken> LockAsync(this SemaphoreSlim semaphore,
CancellationToken cancellationToken = default)
{
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
return new ReleaseToken(semaphore);
}
public readonly struct ReleaseToken : IDisposable
{
private readonly SemaphoreSlim _semaphore;
public ReleaseToken(SemaphoreSlim semaphore) => _semaphore = semaphore;
public void Dispose() => _semaphore?.Release();
}
Usage example (sync/async):
using (semaphore.Lock())
{
DoStuff();
}
using (await semaphore.LockAsync())
{
await DoStuffAsync();
}
The synchronous Lock
is always allocation-free, regardless of whether the semaphore is acquired immediately or after a blocking wait. The asynchronous LockAsync
is also allocation-free, but only when the semaphore is acquired synchronously (when it's CurrentCount
happens to be positive at the time). When there is contention and the LockAsync
must complete asynchronously, 144 bytes are allocated additionally to the standard SemaphoreSlim.WaitAsync
allocations (which are 88 bytes without CancellationToken
, and 497 bytes with cancelable CancellationToken
as of .NET 5 on a 64 bit machine).
From the docs:
The use of the ValueTask<TResult>
type is supported starting with C# 7.0, and is not supported by any version of Visual Basic.
readonly
structs are available beginning with C# 7.2.
Also here is explained why the IDisposable
ReleaseToken
struct is not boxed by the using
statement.
Note: Personally I am not a fan of (mis)using the using
statement for purposes other than releasing unmanaged resources.
async/await
doesn't change how exceptions behave insideasync
method. It only changes how exceptions are propagated outsideasync
method. So, what was right for "sync word" is still right here. – Hawkeyedlock() {}
: blogs.msdn.microsoft.com/ericlippert/2009/03/06/… – Sharlasharleen