CHT009: SemaphoreSlim(1, 1) used as async lock
Cause
A new SemaphoreSlim(1, 1) expression is used to create an async-compatible exclusive lock.
Rule Description
SemaphoreSlim(1, 1) is a common pattern for async-safe mutual exclusion — a semaphore capped at one
permit effectively acts as a mutex that can be acquired with await semaphore.WaitAsync(). It works,
but it is a general-purpose primitive that allocates a Task for every contended wait.
CryptoHives.Foundation.Threading.Async.Pooled.AsyncLock is purpose-built for the same pattern:
- Uses pooled
IValueTaskSourceobjects so contended waits produce zero heap allocations. - Returns a deterministic
Releaserstruct compatible withusingdeclarations andawait using. - Exposes first-class timeout overloads (
LockAsync(TimeSpan)) withoutAsTask()overhead. - Integrates naturally with
IResettableobject pools.
The diagnostic is Info severity — the SemaphoreSlim(1, 1) code is not incorrect — but
switching to AsyncLock is recommended for hot paths.
How to Fix
Option 1: Apply the code fix
Use the IDE lightbulb or dotnet-suggest to apply the "Replace with AsyncLock" code fix.
It will:
- Replace the
new SemaphoreSlim(1, 1)creation expression withnew AsyncLock(). - Update the declared type from
SemaphoreSlimtoAsyncLock(when the type is explicit). - Add
using CryptoHives.Foundation.Threading.Async.Pooled;if not already present.
Option 2: Migrate manually
// Before — SemaphoreSlim(1, 1) pattern
using System.Threading;
public class MyService
{
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
public async Task DoWorkAsync()
{
await _lock.WaitAsync();
try
{
// critical section
}
finally
{
_lock.Release();
}
}
}
// After — AsyncLock pattern
using CryptoHives.Foundation.Threading.Async.Pooled;
public class MyService
{
private readonly AsyncLock _lock = new AsyncLock();
public async Task DoWorkAsync()
{
using (await _lock.LockAsync())
{
// critical section — lock released automatically on dispose
}
}
}
Or with a using declaration (C# 8+):
public async Task DoWorkAsync()
{
using var releaser = await _lock.LockAsync();
// critical section
} // lock released here
When to Suppress
Suppress if you intentionally need SemaphoreSlim's extra features, such as:
- Non-async
Wait()/Release()calls from synchronous code paths. - The
CurrentCount/AvailableWaitHandleproperties. - A permit count greater than 1 (i.e., not being used as a mutex).
#pragma warning disable CHT009
private readonly SemaphoreSlim _sem = new SemaphoreSlim(1, 1); // needs synchronous Wait()
#pragma warning restore CHT009
Example
Violating Code
using System.Threading;
public class Cache
{
private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1); // CHT009
public async Task<string> GetAsync(string key)
{
await _gate.WaitAsync();
try
{
return Compute(key);
}
finally
{
_gate.Release();
}
}
}
Fixed Code
using CryptoHives.Foundation.Threading.Async.Pooled;
public class Cache
{
private readonly AsyncLock _gate = new AsyncLock();
public async Task<string> GetAsync(string key)
{
using (await _gate.LockAsync())
{
return Compute(key);
}
}
}