AsyncReaderWriterLock
A pooled, allocation-free async reader-writer lock that supports multiple concurrent readers or a single exclusive writer using ValueTask-based waiters with cancellation tokens.
Overview
AsyncReaderWriterLock is an async-compatible reader-writer lock. It allows multiple readers to enter the lock concurrently, but only one writer can hold the lock exclusively. Writers are prioritized over readers to prevent writer starvation. One upgradeable reader at a time can share access with multiple other readers. Once the
upgradeable reader is upgraded to writer, it may have to wait until all readers release the lock. An upgradeable reader may release the lock while still upgraded writers are queued for write access.
An additional internal state, UpgradedWriterWithoutReader, is used when an upgradeable reader releases the lock before all concurrent readers have released while an upgrade to writer is in progress. This ensures correct handling of waiting writers under this scenario.
┌─────────────────────────────────────────────────────────────────────────────┐
│ ------------ │
│ | | <-----> READERS │
│ | | <-----> UPGRADEABLE READER + READERS │
│ | IDLE | <-----> UPGRADEABLE READER -----> UPGRADED WRITER --\ │
│ | NO LOCKS | ^ | │
│ | | |------- DEMOTE TO UPGRADEABLE READER <--/ │
│ | | <--------------- DEMOTE TO IDLE WITHOUT READER <--/ │
│ | | <-----> WRITER │
│ ------------ │
└─────────────────────────────────────────────────────────────────────────────┘
Usage
Basic Usage
using CryptoHives.Foundation.Threading.Async.Pooled;
private readonly AsyncReaderWriterLock _rwLock = new AsyncReaderWriterLock();
// Reader
public async Task<Data> ReadDataAsync(CancellationToken ct)
{
using (await _rwLock.ReaderLockAsync(ct))
{
// Multiple readers can hold the lock concurrently
return await FetchDataAsync();
}
}
// Writer
public async Task WriteDataAsync(Data data, CancellationToken ct)
{
using (await _rwLock.WriterLockAsync(ct))
{
// Exclusive access - no other readers or writers
await SaveDataAsync(data);
}
}
// Upgradeable reader
public async Task UpdateIfNeededAsync(CancellationToken ct)
{
using (var upgradeable = await _rwLock.UpgradeableReaderLockAsync(ct))
{
if (NeedsUpdate())
{
using (await upgradeable.UpgradeToWriterLockAsync(ct))
{
await SaveDataAsync();
}
}
}
}
Cache Pattern
private readonly AsyncReaderWriterLock _cacheLock = new AsyncReaderWriterLock();
private Dictionary<string, object> _cache = new();
public async Task<T> GetOrAddAsync<T>(string key, Func<Task<T>> factory, CancellationToken ct)
{
// Try to read first
using (await _cacheLock.ReaderLockAsync(ct))
{
if (_cache.TryGetValue(key, out var cached))
{
return (T)cached;
}
}
// Need to write
using (await _cacheLock.WriterLockAsync(ct))
{
// Double-check after acquiring write lock
if (_cache.TryGetValue(key, out var cached))
{
return (T)cached;
}
var value = await factory();
_cache[key] = value;
return value;
}
}
Constructor
public AsyncReaderWriterLock(
bool runContinuationAsynchronously = true,
IGetPooledManualResetValueTaskSource<Releaser>? pool = null)
| Parameter | Description |
|---|---|
runContinuationAsynchronously |
If true (default), continuations run on the thread pool. |
pool |
Optional custom pool for ValueTaskSource instances used by both readers and writers. |
Properties
| Property | Type | Description |
|---|---|---|
IsReadLockHeld |
bool |
Gets whether one or more readers currently hold the lock. |
IsWriteLockHeld |
bool |
Gets whether the lock is currently held by a writer. |
IsUpgradeableReadLockHeld |
bool |
Gets whether the lock is currently held by an upgradeable reader. |
IsUpgradedWriterLockHeld |
bool |
Gets whether an upgradeable reader is currently upgraded to writer mode. |
CurrentReaderCount |
int |
Gets the number of readers holding the lock. |
WaitingWriterCount |
int |
Gets the number of writers waiting. |
WaitingReaderCount |
int |
Gets the number of readers waiting. |
WaitingUpgradeableReaderCount |
int |
Gets the number of upgradeable readers waiting. |
WaitingUpgradedWritersCount |
int |
Gets the number of upgrade requests waiting for exclusive access. |
RunContinuationAsynchronously |
bool |
Gets or sets whether continuations run asynchronously. |
Methods
ReaderLockAsync
public ValueTask<Releaser> ReaderLockAsync(CancellationToken cancellationToken = default)
Asynchronously acquires a reader lock. Multiple readers can hold the lock concurrently.
ReaderLockAsync (timeout)
public ValueTask<Releaser> ReaderLockAsync(TimeSpan timeout)
Asynchronously acquires a reader lock, or throws TimeoutException if the timeout elapses first.
Throws: TimeoutException if the timeout elapses, OperationCanceledException if the operation is canceled via the cancellation token, ArgumentOutOfRangeException if timeout is negative and not Timeout.InfiniteTimeSpan.
UpgradeableReaderLockAsync
public ValueTask<Releaser> UpgradeableReaderLockAsync(CancellationToken cancellationToken = default)
Asynchronously acquires an upgradeable reader lock. One upgradeable reader can coexist with other readers and may later be promoted to a writer lock.
UpgradeableReaderLockAsync (timeout)
public ValueTask<Releaser> UpgradeableReaderLockAsync(TimeSpan timeout)
Asynchronously acquires an upgradeable reader lock, or throws TimeoutException if the timeout elapses first.
Throws: TimeoutException if the timeout elapses, OperationCanceledException if the operation is canceled via the cancellation token, ArgumentOutOfRangeException if timeout is negative and not Timeout.InfiniteTimeSpan.
WriterLockAsync
public ValueTask<Releaser> WriterLockAsync(CancellationToken cancellationToken = default)
Asynchronously acquires a writer lock. Only one writer can hold the lock.
WriterLockAsync (timeout)
public ValueTask<Releaser> WriterLockAsync(TimeSpan timeout, CancellationToken cancellationToken = default)
Asynchronously acquires a writer lock, or throws TimeoutException if the timeout elapses first.
Throws: TimeoutException if the timeout elapses, OperationCanceledException if the operation is canceled via the cancellation token, ArgumentOutOfRangeException if timeout is negative and not Timeout.InfiniteTimeSpan.
Allocation notes for all timeout overloads:
| Scenario | TimeProvider allocated? |
|---|---|
| Lock immediately available | No |
Timeout.InfiniteTimeSpan |
No |
TimeSpan.Zero and contested |
No (immediate exception) |
| Finite positive timeout | Yes — one instance, disposed on await |
Allocation Behavior
Immediate lock acquisitions via the fast path 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<Releaser> instances are reused to minimize allocation pressure across repeated lock operations.
Example:
try
{
using (await _rwLock.ReaderLockAsync(TimeSpan.FromSeconds(5)))
{
return await ReadDataAsync();
}
}
catch (TimeoutException)
{
HandleTimeout();
}
Releaser
All lock-acquisition methods return a Releaser struct that implements IDisposable and IAsyncDisposable:
using (await _rwLock.ReaderLockAsync())
{
// Lock is held here
}
// Lock is automatically released
When the Releaser originated from UpgradeableReaderLockAsync, it also exposes:
public ValueTask<Releaser> UpgradeToWriterLockAsync(CancellationToken cancellationToken = default)
This upgrades the currently held upgradeable reader to an exclusive writer lock.
Fairness and Priority
- Writer Priority: New readers are queued behind waiting writers to prevent writer starvation
- Reader Batching: When a writer releases, all waiting readers are released together
- FIFO Readers and Writers: Waiting writers and readers are released in order
Performance
- O(1) reader acquisition with fast path when no writers are waiting/holding
- O(1) writer acquisition with fast path when lock is free
- O(n) reader batch release when writer releases
- Zero allocations on the fast path (uncontended) and contended path, unless the ObjectPool is exhausted. A custom pool can be provided to satisfy specific needs.
Benchmark Results
The following benchmarks compare AsyncReaderWriterLock against ReaderWriterLockSlim, Nito.AsyncEx.AsyncReaderWriterLock, Proto.Promises.Threading.AsyncReaderWriterLock, Microsoft.VisualStudio.Threading.AsyncReaderWriterLock, and a reference implementation. Not all implementations support every lock mode; the set of compared implementations varies per benchmark.
Reader Lock Benchmark
Measures the performance of acquiring and releasing reader locks with varying numbers of nested acquisitions. At the lowest iteration count (Iterations = 0), the pooled implementation achieves lower latency than Proto.Promises; from Iterations = 1 onward, Proto.Promises achieves lower per-operation latency, reflecting a lower per-lock-call overhead at the cost of a slightly higher fixed invocation overhead. Both operate with zero allocations. Nito.AsyncEx allocates per acquisition. VS.Threading allocates per acquisition and shows substantially higher latency at all iteration counts.
| Description | Iterations | cancellationType | Mean | Ratio | Allocated |
|---|---|---|---|---|---|
| ReaderLock · RWLockSlim · System | 0 | None | 7.728 ns | 0.49 | - |
| ReaderLock · AsyncRWLock · Pooled | 0 | None | 15.803 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Proto.Promises | 0 | None | 18.267 ns | 1.16 | - |
| ReaderLock · AsyncRWLock · RefImpl | 0 | None | 18.839 ns | 1.19 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 0 | None | 40.865 ns | 2.59 | 320 B |
| ReaderLock · AsyncRWLock · VS.Threading | 0 | None | 224.367 ns | 14.20 | 208 B |
| ReaderLock · AsyncRWLock · Pooled | 0 | NotCancelled | 15.951 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Proto.Promises | 0 | NotCancelled | 18.197 ns | 1.14 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 0 | NotCancelled | 40.386 ns | 2.53 | 320 B |
| ReaderLock · AsyncRWLock · VS.Threading | 0 | NotCancelled | 225.331 ns | 14.13 | 208 B |
| ReaderLock · RWLockSlim · System | 1 | None | 12.326 ns | 0.30 | - |
| ReaderLock · AsyncRWLock · Proto.Promises | 1 | None | 28.524 ns | 0.69 | - |
| ReaderLock · AsyncRWLock · RefImpl | 1 | None | 33.435 ns | 0.80 | - |
| ReaderLock · AsyncRWLock · Pooled | 1 | None | 41.565 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 1 | None | 83.736 ns | 2.01 | 640 B |
| ReaderLock · AsyncRWLock · VS.Threading | 1 | None | 531.027 ns | 12.78 | 416 B |
| ReaderLock · AsyncRWLock · Proto.Promises | 1 | NotCancelled | 28.778 ns | 0.71 | - |
| ReaderLock · AsyncRWLock · Pooled | 1 | NotCancelled | 40.399 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 1 | NotCancelled | 81.648 ns | 2.02 | 640 B |
| ReaderLock · AsyncRWLock · VS.Threading | 1 | NotCancelled | 532.178 ns | 13.17 | 416 B |
| ReaderLock · RWLockSlim · System | 10 | None | 61.963 ns | 0.32 | - |
| ReaderLock · AsyncRWLock · Proto.Promises | 10 | None | 141.332 ns | 0.73 | - |
| ReaderLock · AsyncRWLock · RefImpl | 10 | None | 143.397 ns | 0.74 | - |
| ReaderLock · AsyncRWLock · Pooled | 10 | None | 194.403 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 10 | None | 467.103 ns | 2.40 | 3520 B |
| ReaderLock · AsyncRWLock · VS.Threading | 10 | None | 3,642.898 ns | 18.74 | 2288 B |
| ReaderLock · AsyncRWLock · Proto.Promises | 10 | NotCancelled | 139.359 ns | 0.72 | - |
| ReaderLock · AsyncRWLock · Pooled | 10 | NotCancelled | 194.276 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 10 | NotCancelled | 464.283 ns | 2.39 | 3520 B |
| ReaderLock · AsyncRWLock · VS.Threading | 10 | NotCancelled | 3,640.725 ns | 18.74 | 2288 B |
| ReaderLock · RWLockSlim · System | 100 | None | 570.326 ns | 0.33 | - |
| ReaderLock · AsyncRWLock · Proto.Promises | 100 | None | 1,223.857 ns | 0.70 | - |
| ReaderLock · AsyncRWLock · RefImpl | 100 | None | 1,249.889 ns | 0.71 | - |
| ReaderLock · AsyncRWLock · Pooled | 100 | None | 1,748.960 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 100 | None | 4,435.991 ns | 2.54 | 32320 B |
| ReaderLock · AsyncRWLock · VS.Threading | 100 | None | 86,096.879 ns | 49.23 | 21008 B |
| ReaderLock · AsyncRWLock · Proto.Promises | 100 | NotCancelled | 1,245.661 ns | 0.71 | - |
| ReaderLock · AsyncRWLock · Pooled | 100 | NotCancelled | 1,743.212 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 100 | NotCancelled | 4,404.138 ns | 2.53 | 32320 B |
| ReaderLock · AsyncRWLock · VS.Threading | 100 | NotCancelled | 87,835.002 ns | 50.39 | 21008 B |
Writer Lock Benchmark
Measures the performance of acquiring and releasing a single writer lock. Proto.Promises achieves lower uncontended latency than the pooled implementation, with both operating at zero allocations. Nito.AsyncEx allocates per acquisition. VS.Threading allocates per acquisition and shows substantially higher latency.
| Description | Mean | Ratio | Allocated |
|---|---|---|---|
| WriterLock · RWLockSlim · System | 6.931 ns | 0.62 | - |
| WriterLock · AsyncRWLock · Proto.Promises | 8.353 ns | 0.75 | - |
| WriterLock · AsyncRWLock · Pooled | 11.103 ns | 1.00 | - |
| WriterLock · AsyncRWLock · RefImpl | 18.529 ns | 1.67 | - |
| WriterLock · AsyncRWLock · Nito.AsyncEx | 55.739 ns | 5.02 | 496 B |
| WriterLock · AsyncRWLock · VS.Threading | 1,044.066 ns | 94.04 | 584 B |
Upgradeable Reader Lock Benchmark
Measures the performance of acquiring an upgradeable reader lock in combination with varying numbers of additional reader locks. At the lowest iteration count (Iterations = 0), the pooled implementation is marginally faster; Proto.Promises achieves lower per-operation latency as the number of additional reader locks increases. Both operate with zero allocations. VS.Threading allocates per acquisition and shows substantially higher latency across all iteration counts.
| Description | Iterations | cancellationType | Mean | Ratio | Allocated |
|---|---|---|---|---|---|
| UpgradeableReaderLock · RWLockSlim · System | 0 | None | 6.741 ns | 0.41 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 0 | None | 16.248 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 0 | None | 19.423 ns | 1.20 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 0 | None | 1,064.281 ns | 65.50 | 616 B |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 0 | NotCancelled | 16.334 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 0 | NotCancelled | 17.778 ns | 1.09 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 0 | NotCancelled | 1,135.176 ns | 69.50 | 616 B |
| UpgradeableReaderLock · RWLockSlim · System | 1 | None | 6.725 ns | 0.34 | - |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 1 | None | 17.560 ns | 0.90 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 1 | None | 19.551 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 1 | None | 1,048.830 ns | 53.65 | 616 B |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 1 | NotCancelled | 17.601 ns | 0.92 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 1 | NotCancelled | 19.051 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 1 | NotCancelled | 1,181.340 ns | 62.01 | 616 B |
| UpgradeableReaderLock · RWLockSlim · System | 2 | None | 6.733 ns | 0.36 | - |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 2 | None | 17.646 ns | 0.94 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 2 | None | 18.809 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 2 | None | 1,045.428 ns | 55.58 | 616 B |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 2 | NotCancelled | 17.493 ns | 0.90 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 2 | NotCancelled | 19.365 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 2 | NotCancelled | 1,179.300 ns | 60.90 | 616 B |
| UpgradeableReaderLock · RWLockSlim · System | 5 | None | 24.080 ns | 0.31 | - |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 5 | None | 52.959 ns | 0.69 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 5 | None | 76.546 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 5 | None | 2,477.664 ns | 32.37 | 1240 B |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 5 | NotCancelled | 53.909 ns | 0.72 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 5 | NotCancelled | 74.742 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 5 | NotCancelled | 2,586.760 ns | 34.61 | 1240 B |
Upgraded Writer Lock Benchmark
Measures the performance of acquiring an upgradeable reader lock, holding additional reader locks concurrently, then upgrading to an exclusive writer lock. The pooled implementation is marginally faster at the lowest iteration count (Iterations = 0); Proto.Promises achieves lower per-operation latency as the number of held reader locks increases. Both operate with zero allocations. VS.Threading allocates proportionally to the number of held reader locks and shows substantially higher latency across all configurations.
| Description | Iterations | cancellationType | Mean | Ratio | Allocated |
|---|---|---|---|---|---|
| UpgradedWriterLock · RWLockSlim · System | 0 | None | 13.49 ns | 0.53 | - |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 0 | None | 23.76 ns | 0.93 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 0 | None | 25.64 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 0 | None | 1,716.69 ns | 66.97 | 824 B |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 0 | NotCancelled | 24.74 ns | 0.97 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 0 | NotCancelled | 25.37 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 0 | NotCancelled | 1,785.84 ns | 70.39 | 824 B |
| UpgradedWriterLock · RWLockSlim · System | 1 | None | 34.65 ns | 0.63 | - |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 1 | None | 44.20 ns | 0.81 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 1 | None | 54.81 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 1 | None | 2,233.12 ns | 40.74 | 1032 B |
| UpgradedWriterLock · AsyncRWLock · Pooled | 1 | NotCancelled | 65.71 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 1 | NotCancelled | 69.90 ns | 1.06 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 1 | NotCancelled | 2,269.89 ns | 34.55 | 1032 B |
| UpgradedWriterLock · RWLockSlim · System | 2 | None | 25.47 ns | 0.33 | - |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 2 | None | 53.43 ns | 0.69 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 2 | None | 77.54 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 2 | None | 2,717.26 ns | 35.04 | 1240 B |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 2 | NotCancelled | 80.98 ns | 0.86 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 2 | NotCancelled | 94.63 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 2 | NotCancelled | 2,780.83 ns | 29.39 | 1240 B |
| UpgradedWriterLock · RWLockSlim · System | 5 | None | 43.55 ns | 0.31 | - |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 5 | None | 91.85 ns | 0.66 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 5 | None | 139.13 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 5 | None | 4,235.82 ns | 30.45 | 1864 B |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 5 | NotCancelled | 116.29 ns | 0.80 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 5 | NotCancelled | 146.08 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 5 | NotCancelled | 4,381.78 ns | 30.00 | 1864 B |
Benchmark Analysis
Key Findings:
Reader and upgradeable reader performance: At a single acquisition per call, the pooled implementation has a slight latency advantage. As the number of lock operations per call increases, Proto.Promises achieves lower per-operation latency in both the reader and upgradeable reader benchmarks, with zero allocations in both cases.
Writer and upgraded writer performance: For single-operation writer lock acquisition, Proto.Promises achieves lower uncontended latency than the pooled implementation. The same pattern holds for the upgraded writer benchmark: the pooled implementation is marginally faster at minimal load, while Proto.Promises achieves lower per-operation latency as the number of additionally held reader locks increases. Both implementations operate with zero allocations.
Writer priority: The writer-priority design prevents writer starvation but may reduce reader throughput when writers are frequently queued.
Memory efficiency: A shared pool for readers, upgradeable readers, and writers allows fine-tuned pool sizing. The pooled implementation maintains zero allocations across all published benchmarks, matching Proto.Promises. Nito.AsyncEx allocates per acquisition in the reader and writer benchmarks. VS.Threading allocates per acquisition in all benchmarks, with memory usage growing proportionally to the number of concurrently held locks.
Releaser struct: The value-type
Releaserproduces no allocation for the lock handle itself. Allocations occur only for the pooledIValueTaskSourcewhen the pool is exhausted under sustained contention.
When to Choose AsyncReaderWriterLock:
- Read-heavy workloads with occasional writes
- Cache implementations with read/write patterns
- Document or configuration stores
Best Practices
✓ DO: Use timeout overloads to bound lock-wait time
try
{
using (await _rwLock.WriterLockAsync(TimeSpan.FromSeconds(5)))
{
await SaveDataAsync();
}
}
catch (TimeoutException)
{
HandleTimeout();
}
Design Trade-offs:
- Writer priority may reduce reader throughput under write-heavy loads
- Consider
AsyncLockfor simpler mutex-style locking - For read-only scenarios, no lock is needed
Comparison with ReaderWriterLockSlim
| Feature | AsyncReaderWriterLock | ReaderWriterLockSlim |
|---|---|---|
| Async support | Native | None |
| Allocation overhead | Minimal (pooled) | None (sync) |
| Writer priority | Yes | Configurable |
| Cancellation | Full support | None |
See Also
- Threading Package Overview
- AsyncAutoResetEvent - Auto-reset event variant
- AsyncManualResetEvent - Manual-reset event variant
- AsyncLock - Async mutual exclusion lock
- AsyncCountdownEvent - Async countdown event
- AsyncBarrier - Async barrier synchronization primitive
- AsyncSemaphore - Async semaphore primitive
- Benchmarks - Benchmark description
© 2026 The Keepers of the CryptoHives