Asynchronous Locks
Lock acquisition operation may blocks the caller thread. Reader/writer lock from .NET library doesn't have async versions of lock acquisition methods as well as Monitor. To avoid this, DotNext Threading library provides asynchronous non-blocking alternatives of these locks.
Caution
Non-blocking and blocking locks are two different worlds. It is not recommended to mix these API in the same part of application. The lock acquired with blocking API located in Lock, Monitor or ReaderWriteLockSlim is not aware about the lock acquired asynchronously with AsyncLock, AsyncExclusiveLock or AsyncReaderWriterLock. The only exception is SemaphoreSlim because it contains acquisition methods in blocking and non-blocking manner at the same time.
All non-blocking synchronization mechanisms are optimized in terms of memory allocations. If lock acquisitions are not caused in the same time from different application tasks running concurrently then heap allocation associated with waiting queue will not happen.
Asynchronous locks don't rely on the caller thread. The caller thread never blocks so there is no concept of lock owner thread. As a result, these locks are not reentrant.
It is hard to detect root cause of deadlocks occurred by asynchronous locks so use them carefully.
AsyncLock is a unified representation of the all supported asynchronous locks:
- Exclusive lock
- Shared lock
- Reader lock
- Writer lock
- Semaphore
The only one synchronization object can be shared between blocking and non-blocking representations of the lock.
using DotNext.Threading;
using System.Threading;
var semaphore = new SemaphoreSlim(1, 1);
var syncLock = Lock.Semaphore(semaphore);
var asyncLock = AsyncLock.Semaphore(semaphore);
//thread #1
using (syncLock.Acquire())
{
}
//thread #2
using (await asyncLock.AcquireAsync(CancellationToken.None))
{
}
AsyncLock
implementing IAsyncDisposable interface for graceful shutdown if supported by underlying lock type. The following lock types have graceful shutdown:
Details of graceful shutdown described in related articles.
Built-in Reader/Writer Synchronization
Exclusive lock may not be applicable due to performance reasons for some data types. For example, exclusive lock for dictionary or list is redundant because there are two consumers of these objects: writers and readers.
.NEXT Threading library provides several extension methods for more granular control over synchronization of any reference type:
AcquireReadLockAsync
acquires reader lock asynchronouslyAcquireWriteLockAsync
acquires exclusive lock asynchronously
These methods allow to turn any thread-unsafe object into thread-safe object with precise control in context of multithreading access.
using DotNext.Threading;
using System.Text;
var builder = new StringBuilder();
//reader
using (builder.AcquireReadLockAsync(CancellationToken.None))
{
Console.WriteLine(builder.ToString());
}
//writer
using (builder.AcquireWriteLockAsync(CancellationToken.None))
{
builder.Append("Hello, world!");
}
For more information check extension methods inside of AsyncLockAcquisition class.
Custom synchronization primitive
QueuedSynchronizer<TContext> provides low-level infrastructure for writing custom synchronization primitives for asynchronous code. It uses the same synchronization engine as other primitives shipped with the library: AsyncExclusiveLock, AsyncReaderWriterLock, etc. The following example demonstrates how to write custom async-aware reader-writer lock:
using DotNext.Threading;
// bool indicates lock type:
// false - read lock
// true - write lock
class MyExclusiveLock : QueuedSynchronizer<bool>
{
// = 0 - no lock acquired
// > 0 - read lock
// < 0 - write lock
private int readersCount;
public MyExclusiveLock()
: base(null)
{
}
public ValueTask AcquireReadLockAsync(CancellationToken token)
=> base.AcquireAsync(false, token);
public void ReleaseReadLock(CancellationToken token)
=> base.Release(false);
public ValueTask AcquireWriteLockAsync(CancellationToken token)
=> base.AcquireAsync(true, token);
public void ReleaseWriteLock()
=> base.Release(true);
// write lock cannot be acquired if there is at least one read lock, or single write lock
protected override bool CanAcquire(bool writeLock)
=> writeLock ? readersCount is 0 : readersCount >= 0;
protected override void AcquireCore(bool writeLock)
=> readersCount = writeLock ? -1 : readersCount + 1;
protected override void ReleaseCore(bool writeLock)
=> readersCount = writeLock ? 0 : readersCount - 1;
}
Diagnostics
All synchronization primitives for asynchronous code mostly derive from QueuedSynchronized class that exposes a set of important diagnostics counters:
LockContentionCounter
allows to measure a number of lock contentions detected in the specified time periodLockDurationCounter
allows to measure the amount of time spend by the suspended caller in the suspended state
Debugging
In addition to diagnostics tools, QueuedSynchronized and all its derived classes support a rich set of debugging tools:
TrackSuspendedCallers
method allows to enable tracking information about suspended caller. This method has effect only when building project usingDebug
configurationSetCallerInformation
method allows to associate information with the caller if it will be suspended during the call ofWaitAsync
. This method has effect only when building project usingDebug
configurationGetSuspendedCallers
method allows to capture a list of all suspended callers. The method is working only if tracking is enabled viaTrackSuspendedCallers
method. Typically, this method should be used in debugger's Watch window when all threads are paused