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
poolIGetPooledManualResetValueTaskSource<AsyncLock.Releaser>Custom pool for this instance.
Properties
IsTaken
Whether the lock is currently held.
public bool IsTaken { get; }
Property Value
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
cancellationTokenCancellationTokenThe 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
timeoutTimeSpanThe maximum time to wait. Use InfiniteTimeSpan to wait indefinitely.
cancellationTokenCancellationTokenThe 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
timeoutis negative and not equal to InfiniteTimeSpan.- TimeoutException
Thrown when the timeout elapses before the lock can be acquired.
- OperationCanceledException
Thrown when
cancellationTokenis 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
Remarks
In general, this method is not expected to be thread-safe.