Table of Contents

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 IValueTaskSource objects so contended waits produce zero heap allocations.
  • Returns a deterministic Releaser struct compatible with using declarations and await using.
  • Exposes first-class timeout overloads (LockAsync(TimeSpan)) without AsTask() overhead.
  • Integrates naturally with IResettable object 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:

  1. Replace the new SemaphoreSlim(1, 1) creation expression with new AsyncLock().
  2. Update the declared type from SemaphoreSlim to AsyncLock (when the type is explicit).
  3. 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 / AvailableWaitHandle properties.
  • 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);
        }
    }
}

See Also