Skip to content

Latest commit

 

History

History
134 lines (101 loc) · 6.86 KB

index.md

File metadata and controls

134 lines (101 loc) · 6.86 KB

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 asynchronously
  • AcquireWriteLockAsync 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 period
  • LockDurationCounter 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 using Debug configuration
  • SetCallerInformation method allows to associate information with the caller if it will be suspended during the call of WaitAsync. This method has effect only when building project using Debug configuration
  • GetSuspendedCallers method allows to capture a list of all suspended callers. The method is working only if tracking is enabled via TrackSuspendedCallers method. Typically, this method should be used in debugger's Watch window when all threads are paused