Table of Contents

Class AsyncLock

Namespace
CryptoHives.Foundation.Threading.Async.Pooled
Assembly
CryptoHives.Foundation.Threading.dll

An allocation-free async-compatible exclusive lock implemented with pooled ValueTask sources. Note that this lock is not recursive!

public sealed class AsyncLock : IResettable
Inheritance
AsyncLock
Implements
Inherited Members

Remarks

Optional timeout and cancellation token parameters on LockAsync(TimeSpan, CancellationToken).

Allocation Behavior: Immediate acquisitions are completely allocation-free using atomic operations. When the lock is contended, waiting without a timeout is allocation-free on .NET 6.0+ (using UnsafeRegister for cancellation), while older frameworks may allocate for cancellation registration. Specifying a finite timeout allocates a timer that is automatically disposed when the operation completes. Exception and task allocations occur only if a timeout actually elapses or cancellation is triggered; successful acquisitions are otherwise allocation-free. Pooled IValueTaskSource<TResult> instances are reused to minimize allocation pressure across repeated lock operations.

The vast majority of use cases are to just replace a lock statement. That is, with the original code looking like this:

private readonly object _mutex = new object();
public void DoStuff()
{
    lock (_mutex)
    {
        Thread.Sleep(TimeSpan.FromSeconds(1));
    }
}

To replace the blocking operation Thread.Sleep with an asynchronous equivalent, it's not directly possible because of the lock block. We cannot await inside of a lock.

So, we use the async-compatible AsyncLock instead:

private var _mutex = new AsyncLock();
public async Task DoStuffAsync()
{
    using (await _mutex.LockAsync())
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
    }
}

The IResettable interface is implemented to allow resetting the state of the instance for reuse by an implementation of an ObjectPool that uses the DefaultObjectPool<T> implementation.

Constructors

AsyncLock(IGetPooledManualResetValueTaskSource<Releaser>?)

Constructs a new AsyncLock instance with optional custom pool and custom default queue size.

public AsyncLock(IGetPooledManualResetValueTaskSource<AsyncLock.Releaser>? pool = null)

Parameters

pool IGetPooledManualResetValueTaskSource<AsyncLock.Releaser>

Custom pool for this instance.

Properties

IsTaken

Whether the lock is currently held.

public bool IsTaken { get; }

Property Value

bool

Methods

LockAsync(CancellationToken)

Asynchronously acquires the lock, with a cancellation token. The cancellation token is only observed if the lock can not be acquired immediately.

public ValueTask<AsyncLock.Releaser> LockAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

The cancellation token.

Returns

ValueTask<AsyncLock.Releaser>

A ValueTask<TResult> that completes when the lock is acquired. Dispose the returned releaser to release the lock.

Remarks

Note that this lock is not recursive! The returned ValueTask must be disposed to release the lock. Use the following pattern to synchronize async Tasks.

private readonly var _lock = new AsyncLock();
public async Task DoStuffAsync(CancellationToken ct)
{
    using (await _lock.LockAsync(ct))
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
    }
}

LockAsync(TimeSpan, CancellationToken)

Asynchronously acquires the lock, or throws if the lock cannot be acquired before the timeout elapses.

public ValueTask<AsyncLock.Releaser> LockAsync(TimeSpan timeout, CancellationToken cancellationToken = default)

Parameters

timeout TimeSpan

The maximum time to wait. Use InfiniteTimeSpan to wait indefinitely.

cancellationToken CancellationToken

The cancellation token used to cancel the wait.

Returns

ValueTask<AsyncLock.Releaser>

A ValueTask<TResult> that completes when the lock is acquired. Dispose the returned releaser to release the lock.

Remarks

If the lock is immediately available, the method completes synchronously without allocating any cancellation infrastructure. A CancellationTokenSource is allocated only when the lock cannot be acquired immediately and a finite positive timeout is requested; it is disposed automatically when the returned ValueTask<TResult> is awaited.

Exceptions

ArgumentOutOfRangeException

Thrown when timeout is negative and not equal to InfiniteTimeSpan.

TimeoutException

Thrown when the timeout elapses before the lock can be acquired.

OperationCanceledException

Thrown when cancellationToken is cancelled before the lock can be acquired.

TryReset()

Reset the object to a neutral state, semantically similar to when the object was first constructed.

public bool TryReset()

Returns

bool

true if the object was able to reset itself, otherwise false.

Remarks

In general, this method is not expected to be thread-safe.