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.