This is an alternative to .NET's
ReaderWriterLockSlim
with a similar functionality, but can be used in async methods. Due to its async-readiness, it
does not support recursive locks (see section Differences).
The lock can have different modes:
- Read mode: One or more read mode locks can be active at a time while no write mode lock is active.
- Write mode: One write mode lock can be active at a time while no other write mode locks and no other read mode locks are active.
When a task or thread ("execution flow") tries to enter a write mode lock while at least one read mode lock is active, it is blocked until the last read mode lock is released.
When a task or thread tries to enter a read mode lock while a write mode lock is active, it is blocked until the write mode lock is released.
If, while other read mode locks are active and the current task or thread waits to enter the write mode lock, another task or thread tries to enter a read mode lock, it is blocked until the current task or thread released the write mode lock (or canceled the wait operation), which means writers are favored in this case.
Also, when a write mode lock is released while there are one or more execution flows trying to enter a write mode lock and also one or more execution flows trying to enter a read mode lock, writers are favored.
The lock provides synchronous Enter...()
methods for the different lock modes that block
until the lock has been acquired, and asynchronous Enter...Async()
methods that
"block asynchronously" by returning a
Task
that
will complete once the lock has been acquired.
For each Enter...()
and Enter...Async()
method there is also a TryEnter...()
and
TryEnter...Async()
method that allow you to specify an integer time-out, and return a Boolean
that indicates if the lock could be acquired within that time-out.
You must make sure to call the corresponding Exit...()
method to release the lock once you
don't need it anymore.
Additionally, the AsyncReaderWriterLockSlimExtension
class contains extension methods
that return an IDisposable
so that the lock can be used with a using
block.
When using the AsyncReaderWriterLockSlim
for write mode locks only, performance is
significantly slower than simply using a
SemaphoreSlim
due to the additional overhead. Therefore, use the AsyncReaderWriterLockSlim
only
when there are far more readers than writers.
This implementation has the following differences to .NET's
ReaderWriterLockSlim
:
- The lock is not thread-affine, which means one thread can enter the lock, and a different
thread can release it. This allows you to use the lock in an async method with a
await
operator between entering and releasing the lock. - Additionally to synchronous methods like
EnterReadLock
, it has asynchronous methods likeEnterReadLockAsync
which can be called in async methods, so that the current thread is not blocked while waiting for the lock. - You can specify a
CancellationToken
when entering a lock to cancel the wait operation. - Because this lock is not thread-affine, recursive locks are not supported (which also means they cannot be detected). In order for the lock to work correctly, you must not recursively enter the lock from the same execution flow.
- The lock does not support upgradeable read mode locks that can be upgraded to a write mode lock, due to the complexity this would add.
This implementation has the following differences to Nito.AsyncEx'
AsyncReaderWriterLock
:
- Instead of methods that return an
IDisposable
, it hasEnter...()
andExit...()
methods similar to .NET'sReaderWriterLockSlim
. However, the classAsyncReaderWriterLockSlimExtension
provides extension methods that return anIDisposable
. - Additionally to providing a
CancellationToken
that allows you to cancel the wait operation, you can supply an integer time-out to theTryEnter...()
methods. - When calling one of the
Enter...()
methods with an already canceledCancellationToken
, the method does not try to acquire the lock, but instead throws aOperationCanceledException
, which matches the behavior ofSemaphoreSlim
.
To try to acquire the lock without blocking, you can call one of theTry...
methods without specifying a timeout (or specify a timeout of0
). - You can downgrade a write mode lock to a read mode lock by calling
DowngradeWriteLockToReadLock()
. - Non-async methods do not require a ThreadPool thread to run the unblock logic; instead all
code is executed in the thread that called the synchronous method.
This means e.g. synchronous methods can still work even when the ThreadPool (used for async task continuations) is currently exhausted, but only if you didn't call async methods at the same time that are still waiting to get the lock. - The lock is not fair (just as the underlying
SemaphoreSlim
), which means there is no guarantee in which order threads will acquire the lock (e.g. if multiple threads want to get a write lock at the same time using synchronous methods).
Fairness can actually lead to problems like lock convoys, and shouldn't be needed in most cases.
Note: The supported maximum number of concurrent locks in read mode is limited to int.MaxValue
(2147483647).
The lock internally uses
SemaphoreSlim
to implement wait functionality.
Method | Description |
---|---|
Dispose () |
Releases all resources used by the AsyncReaderWriterLockSlim . |
EnterReadLock (CancellationToken) |
Enters the lock in read mode. |
EnterReadLockAsync (CancellationToken) |
Asynchronously enters the lock in read mode. |
TryEnterReadLock (Int32, CancellationToken) |
Tries to enter the lock in read mode, with an optional integer time-out. |
TryEnterReadLockAsync (Int32, CancellationToken) |
Tries to asynchronously enter the lock in read mode, with an optional integer time-out. |
EnterWriteLock (CancellationToken) |
Enters the lock in write mode. |
EnterWriteLockAsync (CancellationToken) |
Asynchronously enters the lock in write mode. |
TryEnterWriteLock (Int32, CancellationToken) |
Tries to enter the lock in write mode, with an optional integer time-out. |
TryEnterWriteLockAsync (Int32, CancellationToken) |
Tries to asynchronously enter the lock in write mode, with an optional integer time-out. |
DowngradeWriteLockToReadLock () |
Downgrades the lock from write mode to read mode. |
ExitReadLock () |
Exits read mode. |
ExitWriteLock () |
Exits write mode. |
Enter the lock in read mode within an async method:
private async Task TestReadModeAsync(AsyncReaderWriterLockSlim asyncLock)
{
// Asynchronously enter the lock in read mode. The task completes after the lock
// has been acquired.
await asyncLock.EnterReadLockAsync();
try
{
// Use Task.Delay to simulate asynchronous work, which means a different thread
// might continue execution after this point.
await Task.Delay(200);
}
finally
{
asyncLock.ExitReadLock();
}
}
Enter the lock in read mode within an async method within an using
block:
private async Task TestReadModeAsync(AsyncReaderWriterLockSlim asyncLock)
{
// Asynchronously enter the lock in read mode. The task completes after the lock
// has been acquired.
// As the Get...() methods return a IDisposeable, you can use the lock within an
// "using" block.
using (var myLock = await asyncLock.GetReadLockAsync())
{
// Use Task.Delay to simulate asynchronous work, which means a different thread
// might continue execution after this point.
await Task.Delay(200);
}
}
Enter the lock in write mode in a synchronous method, using a timeout:
private void TestWriteMode(AsyncReaderWriterLockSlim asyncLock)
{
// Try to enter the lock within 2 seconds.
if (asyncLock.TryEnterWriteLock(2000))
{
try
{
// Simulate some work...
Thread.Sleep(200);
}
finally
{
asyncLock.ExitWriteLock();
}
}
else
{
// We could not enter the lock within the timeout...
}
}