// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Microsoft.VisualStudio.Threading
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
///
/// A non-blocking lock that allows concurrent access, exclusive access, or concurrent with upgradeability to exclusive access.
///
///
/// We have to use a custom awaitable rather than simply returning Task{LockReleaser} because
/// we have to set CallContext data in the context of the person receiving the lock,
/// which requires that we get to execute code at the start of the continuation (whether we yield or not).
///
///
/// Considering this class to be a state machine, the states are:
///
/// READERS
/// | IDLE | <-----> UPGRADEABLE READER + READERS -----> UPGRADED WRITER --\
/// | NO LOCKS | ^ |
/// | | |--- RE-ENTER CONCURRENCY PREP <--/
/// | | <-----> WRITER
/// -------------
/// ]]>
///
///
public partial class AsyncReaderWriterLock : IDisposable
{
///
/// A time delay to check whether pending writer lock and reader locks forms a deadlock.
///
private static readonly TimeSpan DefaultDeadlockCheckTimeout = TimeSpan.FromSeconds(3);
///
/// The default SynchronizationContext to schedule work after issuing a lock.
///
private static readonly SynchronizationContext DefaultSynchronizationContext = new SynchronizationContext();
///
/// The object to acquire a Monitor-style lock on for all field access on this instance.
///
private readonly object syncObject = new object();
///
/// A JoinableTaskContext used to resolve dependencies between read locks to lead into deadlocks when there is a pending write lock.
///
private readonly JoinableTaskContext? joinableTaskContext;
///
/// A CallContext-local reference to the Awaiter that is on the top of the stack (most recently acquired).
///
private readonly AsyncLocal topAwaiter = new AsyncLocal();
///
/// The set of read locks that are issued and active.
///
///
/// Many readers are allowed concurrently. Also, readers may re-enter read locks (recursively)
/// each of which gets an element in this set.
///
private readonly HashSet issuedReadLocks = new HashSet();
///
/// The set of upgradeable read locks that are issued and active.
///
///
/// Although only one upgradeable read lock can be held at a time, this set may have more
/// than one element because that one lock holder may enter the lock it already possesses
/// multiple times.
///
private readonly HashSet issuedUpgradeableReadLocks = new HashSet();
///
/// The set of write locks that are issued and active.
///
///
/// Although only one write lock can be held at a time, this set may have more
/// than one element because that one lock holder may enter the lock it already possesses
/// multiple times.
/// Although this lock is mutually exclusive, there *may* be elements in the
/// set if the write lock was upgraded from a reader.
/// Also note that some elements in this may themselves be upgradeable readers if they have
/// the flag.
///
private readonly HashSet issuedWriteLocks = new HashSet();
///
/// A queue of readers waiting to obtain the concurrent read lock.
///
private readonly Queue waitingReaders = new Queue();
///
/// A queue of upgradeable readers waiting to obtain a lock.
///
private readonly Queue waitingUpgradeableReaders = new Queue();
///
/// A queue of writers waiting to obtain an exclusive lock.
///
private readonly Queue waitingWriters = new Queue();
///
/// The source of the task, which transitions to completed after
/// the method is called and all issued locks have been released.
///
private readonly TaskCompletionSource completionSource = new TaskCompletionSource();
///
/// The queue of callbacks to invoke when the currently held write lock is totally released.
///
///
/// If the write lock is released to an upgradeable read lock, these callbacks are fired synchronously
/// with respect to the writer who is releasing the lock. Otherwise, the callbacks are invoked
/// asynchronously with respect to the releasing thread.
///
private readonly Queue> beforeWriteReleasedCallbacks = new Queue>();
///
/// A value indicating whether extra resources should be spent to collect diagnostic information
/// that may be useful in deadlock investigations.
///
private bool captureDiagnostics;
///
/// A flag indicating whether we're currently running code to prepare for re-entering concurrency mode
/// after releasing an exclusive lock. The Awaiter being released is the non-null value.
///
private volatile Awaiter? reenterConcurrencyPrepRunning;
///
/// A flag indicating that the method has been called, indicating that no
/// new top-level lock requests should be serviced.
///
private bool completeInvoked;
///
/// A helper class to produce ETW trace events.
///
private EventsHelper etw;
///
/// A timer to recheck potential deadlock caused by pending writer locks.
///
private Timer? pendingWriterLockDeadlockCheckTimer;
///
/// Initializes a new instance of the class.
///
public AsyncReaderWriterLock()
: this(joinableTaskContext: null, captureDiagnostics: false)
{
}
///
/// Initializes a new instance of the class.
///
///
/// true to spend additional resources capturing diagnostic details that can be used
/// to analyze deadlocks or other issues.
public AsyncReaderWriterLock(bool captureDiagnostics)
: this(joinableTaskContext: null, captureDiagnostics)
{
}
///
/// Initializes a new instance of the class.
///
///
/// A JoinableTaskContext to help resolve deadlocks caused by interdependency between top read lock tasks when there is a pending write lock blocking one of them.
///
///
/// true to spend additional resources capturing diagnostic details that can be used
/// to analyze deadlocks or other issues.
public AsyncReaderWriterLock(JoinableTaskContext? joinableTaskContext, bool captureDiagnostics = false)
{
this.etw = new EventsHelper(this);
this.joinableTaskContext = joinableTaskContext;
this.captureDiagnostics = captureDiagnostics;
}
///
/// Flags that modify default lock behavior.
///
[Flags]
public enum LockFlags
{
///
/// The default behavior applies.
///
None = 0x0,
///
/// Causes an upgradeable reader to remain in an upgraded-write state once upgraded,
/// even after the nested write lock has been released.
///
///
/// This is useful when you have a batch of possible write operations to apply, which
/// may or may not actually apply in the end, but if any of them change anything,
/// all of their changes should be seen atomically (within a single write lock).
/// This approach is preferable to simply acquiring a write lock around the batch of
/// potential changes because it doesn't defeat concurrent readers until it knows there
/// is a change to actually make.
///
StickyWrite = 0x1,
}
///
/// An enumeration of the kinds of locks supported by this class.
///
internal enum LockKind
{
///
/// A lock that supports concurrently executing threads that hold this same lock type.
/// Holders of this lock may not obtain a lock without first
/// releasing all their locks.
///
Read,
///
/// A lock that may run concurrently with standard readers, but is exclusive of any other
/// upgradeable readers. Holders of this lock are allowed to obtain a write lock while
/// holding this lock to guarantee continuity of state between what they read and what they write.
///
UpgradeableRead,
///
/// A mutually exclusive lock.
///
Write,
}
///
/// Gets a value indicating whether any kind of lock is held by the caller and can
/// be immediately used given the caller's context.
///
public bool IsAnyLockHeld
{
get { return this.IsReadLockHeld || this.IsUpgradeableReadLockHeld || this.IsWriteLockHeld; }
}
///
/// Gets a value indicating whether any kind of lock is held by the caller without regard
/// to the lock compatibility of the caller's context.
///
public bool IsAnyPassiveLockHeld
{
get { return this.IsPassiveReadLockHeld || this.IsPassiveUpgradeableReadLockHeld || this.IsPassiveWriteLockHeld; }
}
///
/// Gets a value indicating whether the caller holds a read lock.
///
///
/// This property returns false if any other lock type is held, unless
/// within that alternate lock type this lock is also nested.
///
public bool IsReadLockHeld
{
get { return this.IsLockHeld(LockKind.Read); }
}
///
/// Gets a value indicating whether a read lock is held by the caller without regard
/// to the lock compatibility of the caller's context.
///
///
/// This property returns false if any other lock type is held, unless
/// within that alternate lock type this lock is also nested.
///
public bool IsPassiveReadLockHeld
{
get { return this.IsLockHeld(LockKind.Read, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true); }
}
///
/// Gets a value indicating whether the caller holds an upgradeable read lock.
///
///
/// This property returns false if any other lock type is held, unless
/// within that alternate lock type this lock is also nested.
///
public bool IsUpgradeableReadLockHeld
{
get { return this.IsLockHeld(LockKind.UpgradeableRead); }
}
///
/// Gets a value indicating whether an upgradeable read lock is held by the caller without regard
/// to the lock compatibility of the caller's context.
///
///
/// This property returns false if any other lock type is held, unless
/// within that alternate lock type this lock is also nested.
///
public bool IsPassiveUpgradeableReadLockHeld
{
get { return this.IsLockHeld(LockKind.UpgradeableRead, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true); }
}
///
/// Gets a value indicating whether the caller holds a write lock.
///
///
/// This property returns false if any other lock type is held, unless
/// within that alternate lock type this lock is also nested.
///
public bool IsWriteLockHeld
{
get { return this.IsLockHeld(LockKind.Write); }
}
///
/// Gets a value indicating whether a write lock is held by the caller without regard
/// to the lock compatibility of the caller's context.
///
///
/// This property returns false if any other lock type is held, unless
/// within that alternate lock type this lock is also nested.
///
public bool IsPassiveWriteLockHeld
{
get { return this.IsLockHeld(LockKind.Write, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true); }
}
///
/// Gets a task whose completion signals that this lock will no longer issue locks.
///
///
/// This task only transitions to a complete state after a call to .
///
public Task Completion
{
get { return this.completionSource.Task; }
}
///
/// Gets the object used to synchronize access to this instance's fields.
///
protected object SyncObject
{
get { return this.syncObject; }
}
///
/// Gets the lock held by the caller's execution context.
///
protected LockHandle AmbientLock
{
get { return new LockHandle(this.GetFirstActiveSelfOrAncestor(this.topAwaiter.Value)); }
}
///
/// Gets or sets a value indicating whether additional resources should be spent to collect
/// information that would be useful in diagnosing deadlocks, etc.
///
protected bool CaptureDiagnostics
{
get { return this.captureDiagnostics; }
set { this.captureDiagnostics = value; }
}
///
/// Gets a time delay to check whether pending writer lock and reader locks forms a deadlock.
///
protected virtual TimeSpan DeadlockCheckTimeout => DefaultDeadlockCheckTimeout;
///
/// Gets a value indicating whether the current thread is allowed to
/// hold an active lock.
///
///
/// The default implementation of this property returns true
/// when the calling thread is NOT an STA thread.
/// This property may be overridden to return false
/// on threads that may compromise the integrity of the lock.
///
protected virtual bool CanCurrentThreadHoldActiveLock
{
get { return Thread.CurrentThread.GetApartmentState() != ApartmentState.STA; }
}
///
/// Gets a value indicating whether the current SynchronizationContext is one that is not supported
/// by this lock.
///
protected virtual bool IsUnsupportedSynchronizationContext
{
get
{
SynchronizationContext? ctxt = SynchronizationContext.Current;
bool supported = ctxt is null || ctxt is NonConcurrentSynchronizationContext;
return !supported;
}
}
///
/// Obtains a read lock, asynchronously awaiting for the lock if it is not immediately available.
///
///
/// A token whose cancellation indicates lost interest in obtaining the lock.
/// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available,
/// a canceled token will cause the code that is waiting for the lock to resume with an .
///
/// An awaitable object whose result is the lock releaser.
public Awaitable ReadLockAsync(CancellationToken cancellationToken = default(CancellationToken))
{
return new Awaitable(this, LockKind.Read, LockFlags.None, cancellationToken);
}
///
/// Obtains an upgradeable read lock, asynchronously awaiting for the lock if it is not immediately available.
///
///
/// A token whose cancellation indicates lost interest in obtaining the lock.
/// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available,
/// a canceled token will cause the code that is waiting for the lock to resume with an .
///
/// An awaitable object whose result is the lock releaser.
public Awaitable UpgradeableReadLockAsync(CancellationToken cancellationToken = default(CancellationToken))
{
return new Awaitable(this, LockKind.UpgradeableRead, LockFlags.None, cancellationToken);
}
///
/// Obtains a read lock, asynchronously awaiting for the lock if it is not immediately available.
///
/// Modifications to normal lock behavior.
///
/// A token whose cancellation indicates lost interest in obtaining the lock.
/// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available,
/// a canceled token will cause the code that is waiting for the lock to resume with an .
///
/// An awaitable object whose result is the lock releaser.
public Awaitable UpgradeableReadLockAsync(LockFlags options, CancellationToken cancellationToken = default(CancellationToken))
{
return new Awaitable(this, LockKind.UpgradeableRead, options, cancellationToken);
}
///
/// Obtains a write lock, asynchronously awaiting for the lock if it is not immediately available.
///
///
/// A token whose cancellation indicates lost interest in obtaining the lock.
/// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available,
/// a canceled token will cause the code that is waiting for the lock to resume with an .
///
/// An awaitable object whose result is the lock releaser.
public Awaitable WriteLockAsync(CancellationToken cancellationToken = default(CancellationToken))
{
return new Awaitable(this, LockKind.Write, LockFlags.None, cancellationToken);
}
///
/// Obtains a write lock, asynchronously awaiting for the lock if it is not immediately available.
///
/// Modifications to normal lock behavior.
///
/// A token whose cancellation indicates lost interest in obtaining the lock.
/// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available,
/// a canceled token will cause the code that is waiting for the lock to resume with an .
///
/// An awaitable object whose result is the lock releaser.
public Awaitable WriteLockAsync(LockFlags options, CancellationToken cancellationToken = default(CancellationToken))
{
return new Awaitable(this, LockKind.Write, options, cancellationToken);
}
///
/// Prevents use or visibility of the caller's lock(s) until the returned value is disposed.
///
/// The value to dispose to restore lock visibility.
///
/// This can be used by a write lock holder that is about to fork execution to avoid
/// two threads simultaneously believing they hold the exclusive write lock.
/// The lock should be hidden just before kicking off the work and can be restored immediately
/// after kicking off the work.
///
public Suppression HideLocks()
{
return new Suppression(this);
}
///
/// Causes new top-level lock requests to be rejected and the task to transition
/// to a completed state after any issued locks have been released.
///
public void Complete()
{
lock (this.syncObject)
{
this.completeInvoked = true;
this.CompleteIfAppropriate();
}
}
///
/// Registers a callback to be invoked when the write lock held by the caller is
/// about to be ultimately released (outermost write lock).
///
///
/// The asynchronous delegate to invoke.
/// Access to the write lock is provided throughout the asynchronous invocation.
///
///
/// This supports some scenarios VC++ has where change event handlers need to inspect changes,
/// or follow up with other changes to respond to earlier changes, at the conclusion of the lock.
/// This method is safe to call from within a previously registered callback, in which case the
/// registered callback will run when previously registered callbacks have completed execution.
/// If the write lock is released to an upgradeable read lock, these callbacks are fired synchronously
/// with respect to the writer who is releasing the lock. Otherwise, the callbacks are invoked
/// asynchronously with respect to the releasing thread.
///
public void OnBeforeWriteLockReleased(Func action)
{
Requires.NotNull(action, nameof(action));
lock (this.syncObject)
{
if (!this.IsWriteLockHeld)
{
throw new InvalidOperationException();
}
this.beforeWriteReleasedCallbacks.Enqueue(action);
}
}
///
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Disposes managed and unmanaged resources held by this instance.
///
/// true if was called; false if the object is being finalized.
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Timer? timerToDispose = null;
lock (this.syncObject)
{
timerToDispose = this.pendingWriterLockDeadlockCheckTimer;
this.pendingWriterLockDeadlockCheckTimer = null;
}
timerToDispose?.Dispose();
}
}
///
/// Checks whether the aggregated flags from all locks in the lock stack satisfy the specified flag(s).
///
/// The flag(s) that must be specified for a true result.
/// The head of the lock stack to consider.
/// true if all the specified flags are found somewhere in the lock stack; false otherwise.
protected bool LockStackContains(LockFlags flags, LockHandle handle)
{
LockFlags aggregateFlags = LockFlags.None;
Awaiter? awaiter = handle.Awaiter;
if (awaiter is object)
{
lock (this.syncObject)
{
while (awaiter is object)
{
if (this.IsLockActive(awaiter, considerStaActive: true, checkSyncContextCompatibility: true))
{
aggregateFlags |= awaiter.Options;
if ((aggregateFlags & flags) == flags)
{
return true;
}
}
awaiter = awaiter.NestingLock;
}
}
}
return (aggregateFlags & flags) == flags;
}
///
/// Returns the aggregate of the lock flags for all nested locks.
///
///
/// This is not redundant with because that returns fast
/// once the presence of certain flag(s) is determined, whereas this will aggregate all flags,
/// some of which may be defined by derived types.
///
protected LockFlags GetAggregateLockFlags()
{
LockFlags aggregateFlags = LockFlags.None;
Awaiter? awaiter = this.topAwaiter.Value;
if (awaiter is object)
{
lock (this.syncObject)
{
while (awaiter is object)
{
if (this.IsLockActive(awaiter, considerStaActive: true, checkSyncContextCompatibility: true))
{
aggregateFlags |= awaiter.Options;
}
awaiter = awaiter.NestingLock;
}
}
}
return aggregateFlags;
}
///
/// Fired when any lock is being released.
///
/// true if the last write lock that the caller holds is being released; false otherwise.
/// The lock being released.
/// A task whose completion signals the conclusion of the asynchronous operation.
protected virtual Task OnBeforeLockReleasedAsync(bool exclusiveLockRelease, LockHandle releasingLock)
{
// Raise the write release lock event if and only if this is the last write that is about to be released.
// Also check that issued read lock count is 0, because these callbacks themselves may acquire read locks
// on top of this write lock that hasn't quite gone away yet, and when they release their read lock,
// that shouldn't trigger a recursive call of the event.
if (exclusiveLockRelease)
{
return this.OnBeforeExclusiveLockReleasedAsync();
}
else
{
return Task.CompletedTask;
}
}
///
/// Fired when the last write lock is about to be released.
///
/// A task whose completion signals the conclusion of the asynchronous operation.
protected virtual Task OnBeforeExclusiveLockReleasedAsync()
{
lock (this.SyncObject)
{
// While this method is called when the last write lock is about to be released,
// a derived type may override this method and have already taken an additional write lock,
// so only state our assumption in the non-derivation case.
Assumes.True(this.issuedWriteLocks.Count == 1 || !this.GetType().Equals(typeof(AsyncReaderWriterLock)));
if (this.beforeWriteReleasedCallbacks.Count > 0)
{
return this.InvokeBeforeWriteLockReleaseHandlersAsync();
}
else
{
return Task.CompletedTask;
}
}
}
///
/// Get the task scheduler to execute the continuation when the lock is acquired.
/// AsyncReaderWriterLock uses a special to handle execusive locks, and will ignore task scheduler provided, so this is only used in a read lock scenario.
/// This method is called within the execution context to wait the read lock, so it can pick up based on the current execution context.
/// Note: the task scheduler is only used, when the lock is issued later. If the lock is issued immediately when returns true, it will be ignored.
///
/// A task scheduler to schedule the continutation task when a lock is issued.
protected virtual TaskScheduler GetTaskSchedulerForReadLockRequest()
{
return TaskScheduler.Default;
}
///
/// Invoked after an exclusive lock is released but before anyone has a chance to enter the lock.
///
///
/// This method is called while holding a private lock in order to block future lock consumers till this method is finished.
///
protected virtual Task OnExclusiveLockReleasedAsync()
{
return Task.CompletedTask;
}
///
/// Invoked when a top-level upgradeable read lock is released, leaving no remaining (write) lock.
///
protected virtual void OnUpgradeableReadLockReleased()
{
}
///
/// Invoked when the lock detects an internal error or illegal usage pattern that
/// indicates a serious flaw that should be immediately reported to the application
/// and/or bring down the process to avoid hangs or data corruption.
///
/// The exception that captures the details of the failure.
/// An exception that may be returned by some implementations of tis method for he caller to rethrow.
protected virtual Exception OnCriticalFailure(Exception ex)
{
Requires.NotNull(ex, nameof(ex));
Report.Fail(ex.Message);
Environment.FailFast(ex.ToString(), ex);
throw Assumes.NotReachable();
}
///
/// Invoked when the lock detects an internal error or illegal usage pattern that
/// indicates a serious flaw that should be immediately reported to the application
/// and/or bring down the process to avoid hangs or data corruption.
///
/// The message to use for the exception.
/// An exception that may be returned by some implementations of tis method for he caller to rethrow.
protected Exception OnCriticalFailure(string message)
{
try
{
throw Assumes.Fail(message);
}
catch (Exception ex)
{
throw this.OnCriticalFailure(ex);
}
}
///
/// Checks whether the specified lock has any active nested locks.
///
private static bool HasAnyNestedLocks(Awaiter lck, HashSet lockCollection)
{
Requires.NotNull(lck, nameof(lck));
Requires.NotNull(lockCollection, nameof(lockCollection));
if (lockCollection.Count > 0)
{
foreach (Awaiter? nestedCandidate in lockCollection)
{
if (nestedCandidate == lck)
{
// This isn't nested -- it's the lock itself.
continue;
}
for (Awaiter? a = nestedCandidate.NestingLock; a is object; a = a.NestingLock)
{
if (a == lck)
{
return true;
}
}
}
}
return false;
}
private static void PendingWriterLockDeadlockWatchingCallback(object? state)
{
var readerWriterLock = (AsyncReaderWriterLock?)state;
Assumes.NotNull(readerWriterLock);
readerWriterLock.TryInvokeAllDependentReadersIfAppropriate();
lock (readerWriterLock.syncObject)
{
readerWriterLock.pendingWriterLockDeadlockCheckTimer?.Change((int)readerWriterLock.DeadlockCheckTimeout.TotalMilliseconds, -1);
}
}
///
/// Throws an exception if called on an STA thread.
///
private void ThrowIfUnsupportedThreadOrSyncContext()
{
if (!this.CanCurrentThreadHoldActiveLock)
{
Verify.FailOperation(Strings.STAThreadCallerNotAllowed);
}
if (this.IsUnsupportedSynchronizationContext)
{
Verify.FailOperation(Strings.AppliedSynchronizationContextNotAllowed);
}
}
///
/// Gets a value indicating whether the caller's thread apartment model and SynchronizationContext
/// is compatible with a lock.
///
private bool IsLockSupportingContext(Awaiter? awaiter = null)
{
if (!this.CanCurrentThreadHoldActiveLock || this.IsUnsupportedSynchronizationContext)
{
return false;
}
awaiter = awaiter ?? this.topAwaiter.Value;
if (this.IsLockHeld(LockKind.Write, awaiter, allowNonLockSupportingContext: true, checkSyncContextCompatibility: false) ||
this.IsLockHeld(LockKind.UpgradeableRead, awaiter, allowNonLockSupportingContext: true, checkSyncContextCompatibility: false))
{
if (!(SynchronizationContext.Current is NonConcurrentSynchronizationContext))
{
// Upgradeable read and write locks *must* have the NonConcurrentSynchronizationContext applied.
return false;
}
}
return true;
}
///
/// Transitions the task to a completed state
/// if appropriate.
///
private void CompleteIfAppropriate()
{
Assumes.True(Monitor.IsEntered(this.syncObject));
if (this.completeInvoked &&
!this.completionSource.Task.IsCompleted &&
this.reenterConcurrencyPrepRunning is null &&
this.issuedReadLocks.Count == 0 && this.issuedUpgradeableReadLocks.Count == 0 && this.issuedWriteLocks.Count == 0 &&
this.waitingReaders.Count == 0 && this.waitingUpgradeableReaders.Count == 0 && this.waitingWriters.Count == 0)
{
// We must use another task to asynchronously transition this so we don't inadvertently execute continuations inline
// while we're holding a lock.
Task.Run(delegate { this.completionSource.TrySetResult(null); });
}
}
///
/// Detects which lock types the given lock holder has (including all nested locks).
///
/// The most nested lock to be considered.
/// Receives a value indicating whether a read lock is held.
/// Receives a value indicating whether an upgradeable read lock is held.
/// Receives a value indicating whether a write lock is held.
private void AggregateLockStackKinds(Awaiter? awaiter, out bool read, out bool upgradeableRead, out bool write)
{
read = false;
upgradeableRead = false;
write = false;
if (awaiter is object)
{
lock (this.syncObject)
{
while (awaiter is object)
{
// It's possible that this lock has been released (even mid-stack, due to our async nature),
// so only consider locks that are still active.
switch (awaiter.Kind)
{
case LockKind.Read:
read |= this.issuedReadLocks.Contains(awaiter);
break;
case LockKind.UpgradeableRead:
upgradeableRead |= this.issuedUpgradeableReadLocks.Contains(awaiter);
write |= this.IsStickyWriteUpgradedLock(awaiter);
break;
case LockKind.Write:
write |= this.issuedWriteLocks.Contains(awaiter);
break;
}
if (read && upgradeableRead && write)
{
// We've seen it all. Walking the stack further would not provide anything more.
return;
}
awaiter = awaiter.NestingLock;
}
}
}
}
///
/// Gets a value indicating whether all issued locks are merely the top-level lock or nesting locks of the specified lock.
///
/// The most nested lock.
/// true if all issued locks are the specified lock or nesting locks of it.
private bool AllHeldLocksAreByThisStack(Awaiter? awaiter)
{
Assumes.True(awaiter is null || !this.IsLockHeld(LockKind.Write, awaiter)); // this method doesn't yet handle sticky upgraded read locks (that appear in the write lock set).
lock (this.syncObject)
{
if (awaiter is object)
{
int locksMatched = 0;
while (awaiter is object)
{
if (this.GetActiveLockSet(awaiter.Kind).Contains(awaiter))
{
locksMatched++;
}
awaiter = awaiter.NestingLock;
}
return locksMatched == this.issuedReadLocks.Count + this.issuedUpgradeableReadLocks.Count + this.issuedWriteLocks.Count;
}
else
{
return this.issuedReadLocks.Count == 0 && this.issuedUpgradeableReadLocks.Count == 0 && this.issuedWriteLocks.Count == 0;
}
}
}
///
/// Gets a value indicating whether the specified lock is, or is a nested lock of, a given type.
///
/// The kind of lock being queried for.
/// The (possibly nested) lock.
/// true if the lock holder (also) holds the specified kind of lock.
private bool LockStackContains(LockKind kind, Awaiter? awaiter)
{
if (awaiter is object)
{
lock (this.syncObject)
{
HashSet? lockSet = this.GetActiveLockSet(kind);
while (awaiter is object)
{
// It's possible that this lock has been released (even mid-stack, due to our async nature),
// so only consider locks that are still active.
if (awaiter.Kind == kind && lockSet.Contains(awaiter))
{
return true;
}
if (kind == LockKind.Write && this.IsStickyWriteUpgradedLock(awaiter))
{
return true;
}
awaiter = awaiter.NestingLock;
}
}
}
return false;
}
///
/// Checks whether the specified lock is an upgradeable read lock, with a flag,
/// which has actually be upgraded.
///
/// The lock to test.
/// true if the test succeeds; false otherwise.
private bool IsStickyWriteUpgradedLock(Awaiter awaiter)
{
if (awaiter.Kind == LockKind.UpgradeableRead && (awaiter.Options & LockFlags.StickyWrite) == LockFlags.StickyWrite)
{
lock (this.syncObject)
{
return this.issuedWriteLocks.Contains(awaiter);
}
}
return false;
}
///
/// Checks whether the caller's held locks (or the specified lock stack) includes an active lock of the specified type.
/// Always false when called on an STA thread.
///
/// The type of lock to check for.
/// The most nested lock of the caller, or null to look up the caller's lock in the CallContext.
/// true to throw an exception if the caller has an exclusive lock but not an associated SynchronizationContext.
/// true to return true when a lock is held but unusable because of the context of the caller.
/// true if the caller holds active locks of the given type; false otherwise.
private bool IsLockHeld(LockKind kind, Awaiter? awaiter = null, bool checkSyncContextCompatibility = true, bool allowNonLockSupportingContext = false)
{
if (allowNonLockSupportingContext || this.IsLockSupportingContext(awaiter))
{
lock (this.syncObject)
{
awaiter = awaiter ?? this.topAwaiter.Value;
if (checkSyncContextCompatibility)
{
this.CheckSynchronizationContextAppropriateForLock(awaiter);
}
return this.LockStackContains(kind, awaiter);
}
}
return false;
}
///
/// Checks whether a given lock is active.
/// Always false when called on an STA thread.
///
/// The lock to check.
/// if false the return value will always be false if called on an STA thread.
/// true to throw an exception if the caller has an exclusive lock but not an associated SynchronizationContext.
/// true if the lock is currently issued and the caller is not on an STA thread.
private bool IsLockActive(Awaiter awaiter, bool considerStaActive, bool checkSyncContextCompatibility = false)
{
Requires.NotNull(awaiter, nameof(awaiter));
if (considerStaActive || this.IsLockSupportingContext(awaiter))
{
lock (this.syncObject)
{
bool activeLock = this.GetActiveLockSet(awaiter.Kind).Contains(awaiter);
if (checkSyncContextCompatibility && activeLock)
{
this.CheckSynchronizationContextAppropriateForLock(awaiter);
}
return activeLock;
}
}
return false;
}
///
/// Checks whether the specified awaiter's lock type has an associated SynchronizationContext if one is applicable.
///
/// The awaiter whose lock should be considered.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
private void CheckSynchronizationContextAppropriateForLock(Awaiter? awaiter)
{
////bool syncContextRequired = this.LockStackContains(LockKind.UpgradeableRead, awaiter) || this.LockStackContains(LockKind.Write, awaiter);
////if (syncContextRequired) {
//// if (!(SynchronizationContext.Current is NonConcurrentSynchronizationContext)) {
//// Assumes.Fail();
//// }
////}
}
///
/// Immediately issues a lock to the specified awaiter if it is available.
///
/// The awaiter to issue a lock to.
///
/// A value indicating whether this lock was previously queued. false if this is a new just received request.
/// The value is used to determine whether to reject it if has already been called and this
/// is a new top-level request.
///
///
/// Normally, new reader locks are no longer issued when there is a pending writer lock to allow existing reader lock to complete.
/// However, that can lead deadlocks, when tasks with issued lock depending on tasks requiring new read locks to complete.
/// When it is true, new reader locks will be issued even when there is a pending writer lock.
///
/// A value indicating whether the lock was issued.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
private bool TryIssueLock(Awaiter awaiter, bool previouslyQueued, bool skipPendingWriteLockCheck = false)
{
lock (this.syncObject)
{
if (this.completeInvoked && !previouslyQueued)
{
// If this is a new top-level lock request, reject it completely.
if (awaiter.NestingLock is null)
{
awaiter.SetFault(new InvalidOperationException(Strings.LockCompletionAlreadyRequested));
return false;
}
}
bool issued = false;
if (this.reenterConcurrencyPrepRunning is null)
{
if (this.issuedWriteLocks.Count == 0 && this.issuedUpgradeableReadLocks.Count == 0 && this.issuedReadLocks.Count == 0)
{
issued = true;
}
else
{
this.AggregateLockStackKinds(awaiter, out bool hasRead, out bool hasUpgradeableRead, out bool hasWrite);
switch (awaiter.Kind)
{
case LockKind.Read:
if (this.issuedWriteLocks.Count == 0 && (skipPendingWriteLockCheck || this.waitingWriters.Count == 0))
{
issued = true;
}
else if (hasWrite)
{
// We allow STA threads to not have the sync context applied because it never has it applied,
// and a write lock holder is allowed to transition to an STA tread.
// But if an MTA thread has the write lock but not the sync context, then they're likely
// an accidental execution fork that is exposing concurrency inappropriately.
if (this.CanCurrentThreadHoldActiveLock && !(SynchronizationContext.Current is NonConcurrentSynchronizationContext))
{
#if NETFRAMEWORK || NETCOREAPP // Assertion failures crash on .NET Core < 3.0
Report.Fail("Dangerous request for read lock from fork of write lock.");
#endif
Verify.FailOperation(Strings.DangerousReadLockRequestFromWriteLockFork);
}
issued = true;
}
else if (hasRead || hasUpgradeableRead)
{
issued = true;
}
break;
case LockKind.UpgradeableRead:
if (hasUpgradeableRead || hasWrite)
{
issued = true;
}
else if (hasRead)
{
// We cannot issue an upgradeable read lock to folks who have (only) a read lock.
throw new InvalidOperationException(Strings.CannotUpgradeNonUpgradeableLock);
}
#pragma warning disable CA1508 // Avoid dead conditional code
else if (this.issuedUpgradeableReadLocks.Count == 0 && this.issuedWriteLocks.Count == 0)
#pragma warning restore CA1508 // Avoid dead conditional code
{
issued = true;
}
break;
case LockKind.Write:
if (hasWrite)
{
issued = true;
}
else if (hasRead && !hasUpgradeableRead)
{
// We cannot issue a write lock when the caller already holds a read lock.
throw new InvalidOperationException(Strings.CannotUpgradeNonUpgradeableLock);
}
else if (this.AllHeldLocksAreByThisStack(awaiter.NestingLock))
{
issued = true;
Awaiter? stickyWriteAwaiter = this.FindRootUpgradeableReadWithStickyWrite(awaiter);
if (stickyWriteAwaiter is object)
{
// Add the upgradeable reader as a write lock as well.
this.issuedWriteLocks.Add(stickyWriteAwaiter);
}
}
break;
default:
throw Assumes.NotReachable();
}
}
}
if (issued)
{
this.GetActiveLockSet(awaiter.Kind).Add(awaiter);
this.etw.Issued(awaiter);
}
if (!issued)
{
this.etw.WaitStart(awaiter);
// If the lock is immediately available, we don't need to coordinate with other threads.
// But if it is NOT available, we'd have to wait potentially for other threads to do more work.
Debugger.NotifyOfCrossThreadDependency();
}
return issued;
}
}
///
/// Finds the upgradeable reader with flag that is nearest
/// to the top-level lock request held by the given lock holder.
///
/// The awaiter to start the search down the stack from.
/// The least nested upgradeable reader lock with sticky write flag; or null if none was found.
private Awaiter? FindRootUpgradeableReadWithStickyWrite(Awaiter? headAwaiter)
{
if (headAwaiter is null)
{
return null;
}
Awaiter? lowerMatch = this.FindRootUpgradeableReadWithStickyWrite(headAwaiter.NestingLock);
if (lowerMatch is object)
{
return lowerMatch;
}
if (headAwaiter.Kind == LockKind.UpgradeableRead && (headAwaiter.Options & LockFlags.StickyWrite) == LockFlags.StickyWrite)
{
lock (this.syncObject)
{
if (this.issuedUpgradeableReadLocks.Contains(headAwaiter))
{
return headAwaiter;
}
}
}
return null;
}
///
/// Gets the set of locks of a given kind.
///
/// The kind of lock.
/// A set of locks.
private HashSet GetActiveLockSet(LockKind kind)
{
switch (kind)
{
case LockKind.Read:
return this.issuedReadLocks;
case LockKind.UpgradeableRead:
return this.issuedUpgradeableReadLocks;
case LockKind.Write:
return this.issuedWriteLocks;
default:
throw Assumes.NotReachable();
}
}
///
/// Gets the queue for a lock with a given type.
///
/// The kind of lock.
/// A queue.
private Queue GetLockQueue(LockKind kind)
{
switch (kind)
{
case LockKind.Read:
return this.waitingReaders;
case LockKind.UpgradeableRead:
return this.waitingUpgradeableReaders;
case LockKind.Write:
return this.waitingWriters;
default:
throw Assumes.NotReachable();
}
}
///
/// Walks the nested lock stack until it finds an active one.
///
/// The most nested lock to consider. May be null.
/// The first active lock encountered, or null if none.
private Awaiter? GetFirstActiveSelfOrAncestor(Awaiter? awaiter)
{
while (awaiter is object)
{
if (this.IsLockActive(awaiter, considerStaActive: true))
{
break;
}
awaiter = awaiter.NestingLock;
}
return awaiter;
}
///
/// Issues a lock to the specified awaiter and executes its continuation.
/// The awaiter should have already been dequeued.
///
/// The awaiter to issue a lock to and execute.
private void IssueAndExecute(Awaiter awaiter)
{
EventsHelper.WaitStop(awaiter);
Assumes.True(this.TryIssueLock(awaiter, previouslyQueued: true, skipPendingWriteLockCheck: true));
Assumes.True(this.ExecuteOrHandleCancellation(awaiter, stillInQueue: false));
}
///
/// Releases the lock held by the specified awaiter.
///
/// The awaiter holding an active lock.
/// A value indicating whether the lock consumer ended up not executing any work.
///
/// A task that should complete before the releasing thread accesses any resource protected by
/// a lock wrapping the lock being released.
/// The task will always be complete if is true .
/// This method guarantees that the lock is effectively released from the caller, and the
/// can be safely recycled, before the synchronous portion of this method completes.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
private Task ReleaseAsync(Awaiter awaiter, bool lockConsumerCanceled = false)
{
// This method does NOT use the async keyword in its signature to avoid CallContext changes that we make
// causing a fork/clone of the CallContext, which defeats our alloc-free uncontested lock story.
// No one should have any locks to release (and be executing code) if we're in our intermediate state.
// When this test fails, it's because someone had an exclusive lock and allowed concurrently executing
// code to fork off and acquire a read (or upgradeable read?) lock, then outlive the parent write lock.
// This is an illegal pattern both because it means an exclusive lock is used concurrently (while the
// parent write lock is active) and when the write lock is released, it means that the child "read"
// lock suddenly became a "concurrent" lock, but we can't transition all the resources from exclusive
// access to concurrent access while someone is actually holding a lock (as such transition requires
// the lock class itself to have the exclusive lock to protect the resources going through the transition).
Awaiter? illegalConcurrentLock = this.reenterConcurrencyPrepRunning; // capture to local to preserve evidence in a concurrently reset field.
if (illegalConcurrentLock is object)
{
try
{
Assumes.Fail(string.Format(CultureInfo.CurrentCulture, "Illegal concurrent use of exclusive lock. Exclusive lock: {0}, Nested lock that outlived parent: {1}", illegalConcurrentLock, awaiter));
}
catch (Exception ex)
{
throw this.OnCriticalFailure(ex);
}
}
if (!this.IsLockActive(awaiter, considerStaActive: true))
{
return Task.CompletedTask;
}
Task? reenterConcurrentOutsideCode = null;
Task? synchronousCallbackExecution = null;
bool synchronousRequired = false;
Awaiter? remainingAwaiter = null;
Awaiter? topAwaiterAtStart = this.topAwaiter.Value; // do this outside the lock because it's fairly expensive and doesn't require the lock.
lock (this.syncObject)
{
// In case this is a sticky write lock, it may also belong to the write locks issued collection.
bool upgradedStickyWrite = awaiter.Kind == LockKind.UpgradeableRead
&& (awaiter.Options & LockFlags.StickyWrite) == LockFlags.StickyWrite
&& this.issuedWriteLocks.Contains(awaiter);
int writeLocksBefore = this.issuedWriteLocks.Count;
int upgradeableReadLocksBefore = this.issuedUpgradeableReadLocks.Count;
int writeLocksAfter = writeLocksBefore - ((awaiter.Kind == LockKind.Write || upgradedStickyWrite) ? 1 : 0);
int upgradeableReadLocksAfter = upgradeableReadLocksBefore - (awaiter.Kind == LockKind.UpgradeableRead ? 1 : 0);
bool finalExclusiveLockRelease = writeLocksBefore > 0 && writeLocksAfter == 0;
Task callbackExecution = Task.CompletedTask;
if (!lockConsumerCanceled)
{
// Callbacks should be fired synchronously iff the last write lock is being released and read locks are already issued.
// This can occur when upgradeable read locks are held and upgraded, and then downgraded back to an upgradeable read.
callbackExecution = this.OnBeforeLockReleasedAsync(finalExclusiveLockRelease, new LockHandle(awaiter)) ?? Task.CompletedTask;
synchronousRequired = finalExclusiveLockRelease && upgradeableReadLocksAfter > 0;
if (synchronousRequired)
{
synchronousCallbackExecution = callbackExecution;
}
}
if (!lockConsumerCanceled)
{
if (writeLocksAfter == 0)
{
bool fireWriteLockReleased = writeLocksBefore > 0;
bool fireUpgradeableReadLockReleased = upgradeableReadLocksBefore > 0 && upgradeableReadLocksAfter == 0;
if (fireWriteLockReleased || fireUpgradeableReadLockReleased)
{
// The Task.Run is invoked from another method so that C# doesn't allocate the anonymous delegate
// it uses unless we actually are going to invoke it --
if (fireWriteLockReleased)
{
reenterConcurrentOutsideCode = this.DowngradeLockAsync(awaiter, upgradedStickyWrite, fireUpgradeableReadLockReleased, callbackExecution);
}
else if (fireUpgradeableReadLockReleased)
{
this.OnUpgradeableReadLockReleased();
}
}
}
}
if (reenterConcurrentOutsideCode is null)
{
this.OnReleaseReenterConcurrencyComplete(awaiter, upgradedStickyWrite, searchAllWaiters: false);
}
remainingAwaiter = this.GetFirstActiveSelfOrAncestor(topAwaiterAtStart);
}
// Updating the topAwaiter requires touching the CallContext, which significantly increases the perf/GC hit
// for releasing locks. So we prefer to leave a released lock in the context and walk up the lock stack when
// necessary. But we will clean it up if it's the last lock released.
if (remainingAwaiter is null)
{
// This assignment is outside the lock because it doesn't need the lock and it's a relatively expensive call
// that we needn't hold the lock for.
this.topAwaiter.Value = remainingAwaiter;
}
if (synchronousRequired || true)
{ // the "|| true" bit is to force us to always be synchronous when releasing locks until we can get all tests passing the other way.
if (reenterConcurrentOutsideCode is object && (synchronousCallbackExecution is object && !synchronousCallbackExecution.IsCompleted))
{
return Task.WhenAll(reenterConcurrentOutsideCode, synchronousCallbackExecution);
}
else
{
return reenterConcurrentOutsideCode ?? synchronousCallbackExecution ?? Task.CompletedTask;
}
}
else
{
return Task.CompletedTask;
}
}
///
/// Schedules work on a background thread that will prepare protected resource(s) for concurrent access.
///
private async Task DowngradeLockAsync(Awaiter awaiter, bool upgradedStickyWrite, bool fireUpgradeableReadLockReleased, Task beginAfterPrerequisite)
{
Requires.NotNull(awaiter, nameof(awaiter));
Requires.NotNull(beginAfterPrerequisite, nameof(beginAfterPrerequisite));
Exception? prereqException = null;
try
{
await beginAfterPrerequisite.ConfigureAwait(SynchronizationContext.Current is NonConcurrentSynchronizationContext);
}
catch (Exception ex)
{
prereqException = ex;
}
Task onExclusiveLockReleasedTask;
lock (this.syncObject)
{
// Check that no read locks are held. If they are, then that's a sign that
// within this write lock, someone took a read lock that is outliving the nesting
// write lock, which is a very dangerous situation.
if (this.issuedReadLocks.Count > 0)
{
if (this.HasAnyNestedLocks(awaiter))
{
try
{
throw new InvalidOperationException(Strings.WriteLockOutlived);
}
catch (InvalidOperationException ex)
{
this.OnCriticalFailure(ex);
}
}
}
this.reenterConcurrencyPrepRunning = awaiter;
onExclusiveLockReleasedTask = this.OnExclusiveLockReleasedAsync();
}
Exception? onExclusiveLockReleasedTaskException = null;
try
{
await onExclusiveLockReleasedTask.ConfigureAwait(false);
}
catch (Exception ex)
{
onExclusiveLockReleasedTaskException = ex;
}
if (fireUpgradeableReadLockReleased)
{
// This will only fire when the outermost upgradeable read is not itself nested by a write lock,
// and that's by design.
this.OnUpgradeableReadLockReleased();
}
lock (this.syncObject)
{
this.reenterConcurrencyPrepRunning = null;
// Skip updating the call context because we're in a forked execution context that won't
// ever impact the client code, and changing the CallContext now would cause the data to be cloned,
// allocating more memory wastefully.
this.OnReleaseReenterConcurrencyComplete(awaiter, upgradedStickyWrite, searchAllWaiters: true);
}
if (prereqException is object)
{
// rethrow the exception we experienced before, such that it doesn't wipe out its callstack.
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(prereqException).Throw();
}
if (onExclusiveLockReleasedTaskException is object)
{
// rethrow the exception we experienced before, such that it doesn't wipe out its callstack.
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(onExclusiveLockReleasedTaskException).Throw();
}
}
///
/// Checks whether the specified lock has any active nested locks.
///
private bool HasAnyNestedLocks(Awaiter lck)
{
Requires.NotNull(lck, nameof(lck));
Assumes.True(Monitor.IsEntered(this.SyncObject));
return HasAnyNestedLocks(lck, this.issuedReadLocks)
|| HasAnyNestedLocks(lck, this.issuedUpgradeableReadLocks)
|| HasAnyNestedLocks(lck, this.issuedWriteLocks);
}
///
/// Called at the conclusion of releasing an exclusive lock to complete the transition.
///
/// The awaiter being released.
/// A flag indicating whether the lock being released was an upgraded read lock with the sticky write flag set.
/// true to scan the entire queue for pending lock requests that might qualify; used when qualifying locks were delayed for some reason besides lock contention.
private void OnReleaseReenterConcurrencyComplete(Awaiter awaiter, bool upgradedStickyWrite, bool searchAllWaiters)
{
Requires.NotNull(awaiter, nameof(awaiter));
lock (this.syncObject)
{
Assumes.True(this.GetActiveLockSet(awaiter.Kind).Remove(awaiter));
if (upgradedStickyWrite)
{
Assumes.True(awaiter.Kind == LockKind.UpgradeableRead);
Assumes.True(this.issuedWriteLocks.Remove(awaiter));
}
this.CompleteIfAppropriate();
this.TryInvokeLockConsumer(searchAllWaiters);
}
}
///
/// Issues locks to one or more queued lock requests and executes their continuations
/// based on lock availability and policy-based prioritization (writer-friendly, etc.)
///
/// true to scan the entire queue for pending lock requests that might qualify; used when qualifying locks were delayed for some reason besides lock contention.
/// true if any locks were issued; false otherwise.
private bool TryInvokeLockConsumer(bool searchAllWaiters)
{
return this.TryInvokeOneWriterIfAppropriate(searchAllWaiters)
|| this.TryInvokeOneUpgradeableReaderIfAppropriate(searchAllWaiters)
|| this.TryInvokeAllReadersIfAppropriate(searchAllWaiters);
}
///
/// Invokes the final write lock release callbacks, if appropriate.
///
/// A task representing the work of sequentially invoking the callbacks.
private async Task InvokeBeforeWriteLockReleaseHandlersAsync()
{
Assumes.True(Monitor.IsEntered(this.syncObject));
Assumes.True(this.beforeWriteReleasedCallbacks.Count > 0);
await using ((await new Awaitable(this, LockKind.Write, LockFlags.None, CancellationToken.None, checkSyncContextCompatibility: false)).ConfigureAwait(false))
{
await Task.Yield(); // ensure we've yielded to our caller, since the WriteLockAsync will not yield when on an MTA thread.
// We sequentially loop over the callbacks rather than fire them concurrently because each callback
// gets visibility into the write lock, which of course provides exclusivity and concurrency would violate that.
// We also avoid executing the synchronous portions all in a row and awaiting them all
// because that too would violate an individual callback's sense of isolation in a write lock.
List? exceptions = null;
while (this.TryDequeueBeforeWriteReleasedCallback(out Func? callback))
{
try
{
await callback().ConfigureAwait(true);
}
catch (Exception ex)
{
if (exceptions is null)
{
exceptions = new List();
}
exceptions.Add(ex);
}
}
if (exceptions is object)
{
throw new AggregateException(exceptions);
}
}
}
///
/// Dequeues a single write lock release callback if available.
///
/// Receives the callback to invoke, if any.
/// A value indicating whether a callback was available to invoke.
private bool TryDequeueBeforeWriteReleasedCallback([NotNullWhen(true)] out Func? callback)
{
lock (this.syncObject)
{
if (this.beforeWriteReleasedCallbacks.Count > 0)
{
callback = this.beforeWriteReleasedCallbacks.Dequeue();
return true;
}
else
{
callback = null;
return false;
}
}
}
///
/// Stores the specified lock in the CallContext dictionary.
///
/// The awaiter that tracks the lock to grant to the caller.
private void ApplyLockToCallContext(Awaiter? topAwaiter)
{
Awaiter? awaiter = this.GetFirstActiveSelfOrAncestor(topAwaiter);
this.topAwaiter.Value = awaiter;
}
///
/// Issues locks to all queued reader lock requests if there are no issued write locks.
///
/// true to scan the entire queue for pending lock requests that might qualify; used when qualifying locks were delayed for some reason besides lock contention.
/// A value indicating whether any readers were issued locks.
private bool TryInvokeAllReadersIfAppropriate(bool searchAllWaiters)
{
bool invoked = false;
if (this.issuedWriteLocks.Count == 0 && this.waitingWriters.Count == 0)
{
while (this.waitingReaders.Count > 0)
{
Awaiter? pendingReader = this.waitingReaders.Dequeue();
Assumes.True(pendingReader.Kind == LockKind.Read);
this.IssueAndExecute(pendingReader);
invoked = true;
}
}
else if (searchAllWaiters)
{
if (this.TryInvokeAnyWaitersInQueue(this.waitingReaders, breakOnFirstIssue: false))
{
return true;
}
}
return invoked;
}
private void TryInvokeAllDependentReadersIfAppropriate()
{
lock (this.syncObject)
{
if (this.issuedWriteLocks.Count == 0 && this.waitingWriters.Count > 0 && this.waitingReaders.Count > 0 && (this.issuedReadLocks.Count > 0 || this.issuedUpgradeableReadLocks.Count > 0))
{
HashSet? dependentTasks = JoinableTaskDependencyGraph.GetDependentTasksFromCandidates(
this.issuedReadLocks.Concat(this.issuedUpgradeableReadLocks).Where(w => w.AmbientJoinableTask is not null).Select(w => w.AmbientJoinableTask!),
this.waitingReaders.Where(w => w.AmbientJoinableTask is not null).Select(w => w.AmbientJoinableTask!));
if (dependentTasks.Count > 0)
{
int pendingCount = this.waitingReaders.Count;
while (pendingCount-- != 0)
{
Awaiter pendingReader = this.waitingReaders.Dequeue();
JoinableTask? readerContext = pendingReader.AmbientJoinableTask;
if (readerContext is not null && dependentTasks.Contains(readerContext))
{
this.IssueAndExecute(pendingReader);
}
else
{
this.waitingReaders.Enqueue(pendingReader);
}
}
}
}
}
}
///
/// Issues a lock to the next queued upgradeable reader, if no upgradeable read or write locks are currently issued.
///
/// true to scan the entire queue for pending lock requests that might qualify; used when qualifying locks were delayed for some reason besides lock contention.
/// A value indicating whether any upgradeable readers were issued locks.
private bool TryInvokeOneUpgradeableReaderIfAppropriate(bool searchAllWaiters)
{
if (this.issuedUpgradeableReadLocks.Count == 0 && this.issuedWriteLocks.Count == 0)
{
if (this.waitingUpgradeableReaders.Count > 0)
{
Awaiter? pendingUpgradeableReader = this.waitingUpgradeableReaders.Dequeue();
Assumes.True(pendingUpgradeableReader.Kind == LockKind.UpgradeableRead);
this.IssueAndExecute(pendingUpgradeableReader);
return true;
}
}
else if (searchAllWaiters)
{
if (this.TryInvokeAnyWaitersInQueue(this.waitingUpgradeableReaders, breakOnFirstIssue: true))
{
return true;
}
}
return false;
}
///
/// Issues a lock to the next queued writer, if no other locks are currently issued
/// or the last contending read lock was removed allowing a waiting upgradeable reader to upgrade.
///
/// true to scan the entire queue for pending lock requests that might qualify; used when qualifying locks were delayed for some reason besides lock contention.
/// A value indicating whether a writer was issued a lock.
private bool TryInvokeOneWriterIfAppropriate(bool searchAllWaiters)
{
if (this.issuedReadLocks.Count == 0 && this.issuedUpgradeableReadLocks.Count == 0 && this.issuedWriteLocks.Count == 0)
{
if (this.waitingWriters.Count > 0)
{
Awaiter? pendingWriter = this.waitingWriters.Dequeue();
if (this.waitingWriters.Count == 0)
{
this.StopPendingWriterLockDeadlockWatching();
}
Assumes.True(pendingWriter.Kind == LockKind.Write);
this.IssueAndExecute(pendingWriter);
return true;
}
}
else if (this.issuedUpgradeableReadLocks.Count > 0 || searchAllWaiters)
{
if (this.TryInvokeAnyWaitersInQueue(this.waitingWriters, breakOnFirstIssue: true))
{
return true;
}
}
return false;
}
///
/// Scans a lock awaiter queue for any that can be issued locks now.
///
/// The queue to scan.
/// true to break out immediately after issuing the first lock.
/// true if any lock was issued; false otherwise.
private bool TryInvokeAnyWaitersInQueue(Queue waiters, bool breakOnFirstIssue)
{
Requires.NotNull(waiters, nameof(waiters));
bool invoked = false;
bool invokedThisLoop;
do
{
invokedThisLoop = false;
foreach (Awaiter? lockWaiter in waiters)
{
if (this.TryIssueLock(lockWaiter, previouslyQueued: true))
{
// Run the continuation asynchronously (since this is called in OnCompleted, which is an async pattern).
Assumes.True(this.ExecuteOrHandleCancellation(lockWaiter, stillInQueue: true));
invoked = true;
invokedThisLoop = true;
if (breakOnFirstIssue)
{
return true;
}
EventsHelper.WaitStop(lockWaiter);
// At this point, the waiter was removed from the queue, so we can't keep
// enumerating the queue or we'll get an InvalidOperationException.
// Break out of the foreach, but the while loop will re-enter and we'll
// examine other possibilities.
break;
}
}
}
while (invokedThisLoop); // keep looping while we find matching locks.
return invoked;
}
///
/// Issues a lock to a lock waiter and execute its code if the lock is immediately available, otherwise
/// queues the lock request.
///
/// The lock request.
private void PendAwaiter(Awaiter awaiter)
{
lock (this.syncObject)
{
if (this.TryIssueLock(awaiter, previouslyQueued: true))
{
// Run the continuation asynchronously (since this is called in OnCompleted, which is an async pattern).
Assumes.True(this.ExecuteOrHandleCancellation(awaiter, stillInQueue: false));
}
else
{
Queue? queue = this.GetLockQueue(awaiter.Kind);
queue.Enqueue(awaiter);
if (awaiter.Kind == LockKind.Write)
{
this.StartPendingWriterDeadlockTimerIfNecessary();
}
}
}
}
private void StartPendingWriterDeadlockTimerIfNecessary()
{
if (this.joinableTaskContext is not null &&
this.pendingWriterLockDeadlockCheckTimer is null &&
this.waitingWriters.Count > 0 &&
(this.issuedReadLocks.Count > 0 || this.issuedUpgradeableReadLocks.Count > 0))
{
this.pendingWriterLockDeadlockCheckTimer = new Timer(PendingWriterLockDeadlockWatchingCallback, this, (int)this.DeadlockCheckTimeout.TotalMilliseconds, -1);
}
}
private void StopPendingWriterLockDeadlockWatching()
{
if (this.pendingWriterLockDeadlockCheckTimer is not null)
{
this.pendingWriterLockDeadlockCheckTimer.Dispose();
this.pendingWriterLockDeadlockCheckTimer = null;
}
}
///
/// Executes the lock receiver or releases the lock because the request for it was canceled before it was issued.
///
/// The awaiter.
/// A value indicating whether the specified is expected to still be in the queue (and should be removed).
/// A value indicating whether a continuation delegate was actually invoked.
private bool ExecuteOrHandleCancellation(Awaiter awaiter, bool stillInQueue)
{
Requires.NotNull(awaiter, nameof(awaiter));
lock (this.SyncObject)
{
if (stillInQueue)
{
// The lock class can't deal well with cancelled lock requests remaining in its queue.
// Remove the awaiter, wherever in the queue it happens to be.
Queue? queue = this.GetLockQueue(awaiter.Kind);
if (!queue.RemoveMidQueue(awaiter))
{
// This can happen when the lock request is cancelled, but during a race
// condition where the lock was just about to be issued anyway.
Assumes.True(awaiter.CancellationToken.IsCancellationRequested);
return false;
}
}
return awaiter.TryScheduleContinuationExecution();
}
}
///
/// An awaitable that is returned from asynchronous lock requests.
///
public readonly struct Awaitable
{
///
/// The awaiter to return from the method.
///
private readonly Awaiter? awaiter;
///
/// Initializes a new instance of the struct.
///
/// The lock class that created this instance.
/// The type of lock being requested.
/// Any flags applied to the lock request.
/// The cancellation token.
/// true to throw an exception if the caller has an exclusive lock but not an associated SynchronizationContext.
internal Awaitable(AsyncReaderWriterLock lck, LockKind kind, LockFlags options, CancellationToken cancellationToken, bool checkSyncContextCompatibility = true)
{
if (checkSyncContextCompatibility)
{
lck.CheckSynchronizationContextAppropriateForLock(lck.topAwaiter.Value);
}
this.awaiter = new Awaiter(lck, kind, options, cancellationToken);
if (!cancellationToken.IsCancellationRequested)
{
lck.TryIssueLock(this.awaiter, previouslyQueued: false);
}
}
///
/// Gets the awaiter value.
///
public Awaiter GetAwaiter()
{
if (this.awaiter is null)
{
throw new InvalidOperationException();
}
return this.awaiter;
}
}
///
/// A value whose disposal releases a held lock.
///
[DebuggerDisplay("{awaiter.kind}")]
public readonly struct Releaser : IDisposable, System.IAsyncDisposable
{
///
/// The awaiter who manages the lifetime of a lock.
///
private readonly Awaiter? awaiter;
///
/// Initializes a new instance of the struct.
///
/// The awaiter.
internal Releaser(Awaiter awaiter)
{
this.awaiter = awaiter;
}
///
/// Releases the lock.
///
public void Dispose()
{
if (this.awaiter is object)
{
var nonConcurrentSyncContext = SynchronizationContext.Current as NonConcurrentSynchronizationContext;
// NOTE: when we have already called ReleaseAsync, and the lock has been released,
// we don't want to load the concurrent context and try to take it back immediately. If we do, it is possible
// that anther thread waiting for a write lock can take the concurrent context, so the current thread will be
// blocked and wait until it is done, and that makes it possible to run into the thread pool exhaustion trap.
if (!this.awaiter.IsReleased)
{
using (nonConcurrentSyncContext is object ? nonConcurrentSyncContext.LoanBackAnyHeldResource(this.awaiter.OwningLock) : default(NonConcurrentSynchronizationContext.LoanBack))
{
Task? releaseTask = this.awaiter.ReleaseAsync();
using (NoMessagePumpSyncContext.Default.Apply())
{
try
{
while (!releaseTask.Wait(1000))
{ // this loop allows us to break into the debugger and step into managed code to analyze a hang.
}
}
catch (AggregateException)
{
// We want to throw the inner exception itself -- not the AggregateException.
releaseTask.GetAwaiter().GetResult();
}
}
}
}
if (nonConcurrentSyncContext is object && !this.awaiter.OwningLock.AmbientLock.IsValid)
{
// The lock holder is taking the synchronous path to release the last UR/W lock held.
// Since they may go synchronously on their merry way for a while, forcibly release
// the sync context's semaphore that they otherwise would hold until their synchronous
// method returns.
nonConcurrentSyncContext.EarlyExitSynchronizationContext();
}
}
}
///
/// Releases the lock.
///
public async ValueTask DisposeAsync()
{
await this.ReleaseAsync().ConfigureAwaitRunInline();
this.Dispose();
}
///
/// Asynchronously releases the lock. Dispose should still be called after this.
///
///
/// A task that should complete before the releasing thread accesses any resource protected by
/// a lock wrapping the lock being released.
///
///
/// Rather than calling this method explicitly, use the C# 8 "await using" syntax instead.
///
public Task ReleaseAsync()
{
if (this.awaiter is object)
{
var nonConcurrentSyncContext = SynchronizationContext.Current as NonConcurrentSynchronizationContext;
using (nonConcurrentSyncContext is object ? nonConcurrentSyncContext.LoanBackAnyHeldResource(this.awaiter.OwningLock) : default(NonConcurrentSynchronizationContext.LoanBack))
{
return this.awaiter.ReleaseAsync();
}
}
return Task.CompletedTask;
}
}
///
/// A value whose disposal restores visibility of any locks held by the caller.
///
public readonly struct Suppression : IDisposable
{
///
/// The locking class.
///
private readonly AsyncReaderWriterLock? lck;
///
/// The awaiter most recently acquired by the caller before hiding locks.
///
private readonly Awaiter? awaiter;
///
/// Initializes a new instance of the struct.
///
/// The lock class.
internal Suppression(AsyncReaderWriterLock lck)
{
this.lck = lck;
this.awaiter = this.lck.topAwaiter.Value;
if (this.awaiter is object)
{
this.lck.topAwaiter.Value = null;
}
}
///
/// Restores visibility of hidden locks.
///
public void Dispose()
{
if (this.lck is object)
{
this.lck.ApplyLockToCallContext(this.awaiter);
}
}
}
///
/// A "public" representation of a specific lock.
///
protected readonly struct LockHandle
{
///
/// The awaiter this lock handle wraps.
///
private readonly Awaiter? awaiter;
///
/// Initializes a new instance of the struct.
///
internal LockHandle(Awaiter? awaiter)
{
this.awaiter = awaiter;
}
///
/// Gets a value indicating whether this handle is to a lock which was actually acquired.
///
public bool IsValid
{
get { return this.awaiter is object; }
}
///
/// Gets a value indicating whether this lock is still active.
///
public bool IsActive
{
get { return this.IsValid && this.awaiter!.OwningLock.IsLockActive(this.awaiter, considerStaActive: true); }
}
///
/// Gets a value indicating whether this lock represents a read lock.
///
public bool IsReadLock
{
get { return this.IsValid ? this.awaiter!.Kind == LockKind.Read : false; }
}
///
/// Gets a value indicating whether this lock represents an upgradeable read lock.
///
public bool IsUpgradeableReadLock
{
get { return this.IsValid ? this.awaiter!.Kind == LockKind.UpgradeableRead : false; }
}
///
/// Gets a value indicating whether this lock represents a write lock.
///
public bool IsWriteLock
{
get { return this.IsValid ? this.awaiter!.Kind == LockKind.Write : false; }
}
///
/// Gets a value indicating whether this lock is an active read lock or is nested by one.
///
public bool HasReadLock
{
get { return this.IsValid ? this.awaiter!.OwningLock.IsLockHeld(LockKind.Read, this.awaiter, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true) : false; }
}
///
/// Gets a value indicating whether this lock is an active upgradeable read lock or is nested by one.
///
public bool HasUpgradeableReadLock
{
get { return this.IsValid ? this.awaiter!.OwningLock.IsLockHeld(LockKind.UpgradeableRead, this.awaiter, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true) : false; }
}
///
/// Gets a value indicating whether this lock is an active write lock or is nested by one.
///
public bool HasWriteLock
{
get { return this.IsValid ? this.awaiter!.OwningLock.IsLockHeld(LockKind.Write, this.awaiter, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true) : false; }
}
///
/// Gets the flags that were passed into this lock.
///
public LockFlags Flags
{
get { return this.IsValid ? this.awaiter!.Options : LockFlags.None; }
}
///
/// Gets or sets some object associated to this specific lock.
///
public object? Data
{
get
{
return this.IsValid ? this.awaiter!.Data : null;
}
set
{
Verify.Operation(this.IsValid, Strings.InvalidLock);
this.awaiter!.Data = value;
}
}
///
/// Gets the lock within which this lock was acquired.
///
public LockHandle NestingLock
{
get { return this.IsValid ? new LockHandle(this.awaiter!.NestingLock) : default(LockHandle); }
}
///
/// Gets the wrapped awaiter.
///
internal Awaiter? Awaiter
{
get { return this.awaiter; }
}
}
///
/// Manages asynchronous access to a lock.
///
[DebuggerDisplay("{kind}")]
public class Awaiter : ICriticalNotifyCompletion
{
///
/// A singleton delegate for use in cancellation token registration to avoid memory allocations for delegates each time.
///
private static readonly Action CancellationResponseAction = CancellationResponder;
///
/// The instance of the lock class to which this awaiter is affiliated.
///
private AsyncReaderWriterLock lck;
///
/// The type of lock requested.
///
private LockKind kind;
///
/// The "parent" lock (i.e. the lock within which this lock is nested) if any.
///
private Awaiter? nestingLock;
///
/// The cancellation token that would terminate waiting for a lock that is not yet available.
///
private CancellationToken cancellationToken;
///
/// The cancellation token event that should be disposed of to free memory when we no longer need to receive cancellation notifications.
///
private CancellationTokenRegistration cancellationRegistration;
///
/// The flags applied to this lock.
///
private LockFlags options;
///
/// Any exception to throw back to the lock requestor.
///
private Exception? fault;
///
/// The continuation to execute when the lock is available.
///
private Action? continuation;
///
/// The continuation we invoked to an issued lock.
///
///
/// We retain this value simply so that in hang reports we can identify the method we issued the lock to.
///
private Action? continuationAfterLockIssued;
///
/// The TaskScheduler to invoke the continuation.
///
private TaskScheduler? continuationTaskScheduler;
///
/// The task from a prior call to , if any.
///
private Task? releaseAsyncTask;
///
/// The synchronization context applied to folks who hold the lock.
///
private SynchronizationContext? synchronizationContext;
///
/// The stacktrace of the caller originally requesting the lock.
///
///
/// This field is initialized only when is constructed with
/// the captureDiagnostics parameter set to true .
///
private StackTrace? requestingStackTrace;
///
/// An arbitrary object that may be set by a derived type of the containing lock class.
///
private object? data;
///
/// Initializes a new instance of the class.
///
/// The lock class creating this instance.
/// The type of lock being requested.
/// The flags to apply to the lock.
/// The cancellation token.
internal Awaiter(AsyncReaderWriterLock lck, LockKind kind, LockFlags options, CancellationToken cancellationToken)
{
Requires.NotNull(lck, nameof(lck));
this.lck = lck;
this.kind = kind;
this.options = options;
this.cancellationToken = cancellationToken;
this.nestingLock = lck.GetFirstActiveSelfOrAncestor(lck.topAwaiter.Value);
this.requestingStackTrace = lck.captureDiagnostics ? new StackTrace(2, true) : null;
this.AmbientJoinableTask = (this.nestingLock is null && this.kind != LockKind.Write) ? this.lck.joinableTaskContext?.AmbientTask : null;
}
///
/// Gets a value indicating whether the lock has been issued.
///
public bool IsCompleted
{
get
{
if (this.fault is object)
{
return true;
}
// If lock has already been issued, we have to switch to the right context, and ignore the CancellationToken.
if (this.lck.IsLockActive(this, considerStaActive: true))
{
return this.lck.IsLockSupportingContext(this);
}
return this.cancellationToken.IsCancellationRequested;
}
}
///
/// Gets the lock instance that owns this awaiter.
///
internal AsyncReaderWriterLock OwningLock
{
get { return this.lck; }
}
///
/// Gets the stack trace of the requestor of this lock.
///
///
/// Used for diagnostic purposes only.
///
internal StackTrace? RequestingStackTrace
{
get { return this.requestingStackTrace; }
}
///
/// Gets the delegate to invoke (or that was invoked) when the lock is/was issued, if available.
/// FOR DIAGNOSTIC PURPOSES ONLY.
///
internal Delegate? LockRequestingContinuation
{
get { return this.continuation ?? this.continuationAfterLockIssued; }
}
///
/// Gets the lock that the caller held before requesting this lock.
///
internal Awaiter? NestingLock
{
get { return this.nestingLock; }
}
///
/// Gets or sets an arbitrary object that may be set by a derived type of the containing lock class.
///
internal object? Data
{
get { return this.data; }
set { this.data = value; }
}
///
/// Gets the cancellation token.
///
internal CancellationToken CancellationToken
{
get { return this.cancellationToken; }
}
///
/// Gets the kind of lock being requested.
///
internal LockKind Kind
{
get { return this.kind; }
}
///
/// Gets the flags applied to this lock.
///
internal LockFlags Options
{
get { return this.options; }
}
///
/// Gets a value indicating whether the lock has already been released.
///
internal bool IsReleased
{
get
{
return this.releaseAsyncTask is object && this.releaseAsyncTask.Status == TaskStatus.RanToCompletion;
}
}
///
/// Gets the ambient JoinableTask when the lock is requested. This is used to resolve deadlock caused by issued read lock depending on new read lock requests blocked by pending write locks.
///
internal JoinableTask? AmbientJoinableTask { get; }
///
/// Gets a value indicating whether the lock is active.
///
/// true iff the lock has bee issued, has not yet been released, and the caller is on an MTA thread.
private bool LockIssued
{
get { return this.lck.IsLockActive(this, considerStaActive: false); }
}
///
/// Sets the delegate to execute when the lock is available.
///
/// The delegate.
public void OnCompleted(Action continuation) => this.OnCompleted(continuation, flowExecutionContext: true);
///
/// Sets the delegate to execute when the lock is available
/// without flowing ExecutionContext.
///
/// The delegate.
public void UnsafeOnCompleted(Action continuation) => this.OnCompleted(continuation, flowExecutionContext: false);
///
/// Applies the issued lock to the caller and returns the value used to release the lock.
///
/// The value to dispose of to release the lock.
public Releaser GetResult()
{
try
{
this.cancellationRegistration.Dispose();
if (!this.LockIssued && this.continuation is null && !this.cancellationToken.IsCancellationRequested)
{
using (var synchronousBlock = new ManualResetEventSlim())
{
this.OnCompleted(synchronousBlock.Set);
synchronousBlock.Wait(this.cancellationToken);
}
}
if (this.fault is object)
{
throw this.fault;
}
if (this.LockIssued)
{
this.lck.ThrowIfUnsupportedThreadOrSyncContext();
if ((this.Kind & (LockKind.UpgradeableRead | LockKind.Write)) != 0)
{
Assumes.True(SynchronizationContext.Current is NonConcurrentSynchronizationContext);
}
this.lck.ApplyLockToCallContext(this);
return new Releaser(this);
}
else if (this.cancellationToken.IsCancellationRequested)
{
// At this point, someone called GetResult who wasn't registered as a synchronous waiter,
// and before the lock was issued.
// If the cancellation token was signaled, we'll throw that because a canceled token is a
// legit reason to hit this path in the method. Otherwise it's an internal error.
throw new OperationCanceledException();
}
this.lck.ThrowIfUnsupportedThreadOrSyncContext();
throw Assumes.NotReachable();
}
catch (OperationCanceledException)
{
// Don't release at this point, or else it would recycle this instance prematurely
// (while it's still in the queue to receive a lock).
throw;
}
catch
{
this.ReleaseAsync(lockConsumerCanceled: true);
throw;
}
}
///
/// Releases the lock and recycles this instance.
///
internal Task ReleaseAsync(bool lockConsumerCanceled = false)
{
if (this.releaseAsyncTask is null)
{
// This method does NOT use the async keyword in its signature to avoid CallContext changes that we make
// causing a fork/clone of the CallContext, which defeats our alloc-free uncontested lock story.
try
{
this.continuationAfterLockIssued = null; // clear field to defend against leaks if Awaiters live a long time.
this.releaseAsyncTask = this.lck.ReleaseAsync(this, lockConsumerCanceled);
}
catch (Exception ex)
{
// An exception here is *really* bad, because a project lock will get orphaned and
// a deadlock will soon result.
// Do what we can to save some evidence by capturing the exception in a faulted task.
// We don't need to rethrow the exception because we return the faulted task.
var tcs = new TaskCompletionSource();
tcs.SetException(ex);
this.releaseAsyncTask = tcs.Task;
}
}
return this.releaseAsyncTask;
}
///
/// Executes the code that requires the lock.
///
/// true if the continuation was (asynchronously) invoked; false if there was no continuation available to invoke.
internal bool TryScheduleContinuationExecution()
{
Action? continuation = Interlocked.Exchange(ref this.continuation, null);
if (continuation is object)
{
this.continuationAfterLockIssued = continuation;
SynchronizationContext? synchronizationContext = this.GetEffectiveSynchronizationContext();
if (this.continuationTaskScheduler is object && synchronizationContext == DefaultSynchronizationContext)
{
Task.Factory.StartNew(continuation, CancellationToken.None, TaskCreationOptions.PreferFairness, this.continuationTaskScheduler);
}
else
{
synchronizationContext.Post(state => ((Action)state!)(), continuation);
}
return true;
}
else
{
return false;
}
}
///
/// Specifies the exception to throw from .
///
internal void SetFault(Exception ex)
{
this.fault = ex;
}
///
/// Responds to lock request cancellation.
///
/// The instance being canceled.
private static void CancellationResponder(object state)
{
var awaiter = (Awaiter)state;
// We're in a race with the lock suddenly becoming available.
// Our control in the race is asking the lock class to execute for us (within their private lock).
// unblock the awaiter immediately (which will then experience an OperationCanceledException).
if (awaiter.lck.ExecuteOrHandleCancellation(awaiter, stillInQueue: true))
{
// A pending write lock can block read locks, so we need issue them when the request is cancelled.
if (awaiter.Kind == LockKind.Write)
{
lock (awaiter.OwningLock.SyncObject)
{
awaiter.OwningLock.TryInvokeLockConsumer(searchAllWaiters: false);
}
}
}
// Release memory of the registered handler, since we only need it to fire once.
awaiter.cancellationRegistration.Dispose();
}
///
/// Get the correct SynchronizationContext to execute code executing within the lock.
/// Note: we need get the NonConcurrentSynchronizationContext from the nesting exclusive lock, because the child lock is essentially under the same context.
/// When we don't have a valid nesting lock, we will create a new NonConcurrentSynchronizationContext for an exclusive lock. For read lock, we don't put it within a NonConcurrentSynchronizationContext,
/// we set it to DefaultSynchronizationContext to mark we have computed it. The result is cached.
///
private SynchronizationContext GetEffectiveSynchronizationContext()
{
if (this.synchronizationContext is null)
{
// Only read locks can be executed trivially. The locks that have some level of exclusivity (upgradeable read and write)
// must be executed via the NonConcurrentSynchronizationContext.
SynchronizationContext? synchronizationContext = null;
Awaiter? awaiter = this.NestingLock;
while (awaiter is object)
{
if (this.lck.IsLockActive(awaiter, considerStaActive: true))
{
synchronizationContext = awaiter.GetEffectiveSynchronizationContext();
break;
}
awaiter = awaiter.NestingLock;
}
if (synchronizationContext is null)
{
if (this.kind == LockKind.Read)
{
// We use DefaultSynchronizationContext to indicate that we have already computed the synchronizationContext once, and prevent repeating this logic second time.
synchronizationContext = DefaultSynchronizationContext;
}
else
{
synchronizationContext = new NonConcurrentSynchronizationContext();
}
}
Interlocked.CompareExchange(ref this.synchronizationContext, synchronizationContext, null);
}
return this.synchronizationContext;
}
///
/// Sets the delegate to execute when the lock is available.
///
/// The delegate.
/// A value indicating whether to flow ExecutionContext.
private void OnCompleted(Action continuation, bool flowExecutionContext)
{
if (this.LockIssued)
{
throw new InvalidOperationException();
}
if (Interlocked.CompareExchange(ref this.continuation, continuation, null) is object)
{
throw new NotSupportedException(Strings.MultipleContinuationsNotSupported);
}
bool restoreFlow = !flowExecutionContext && !ExecutionContext.IsFlowSuppressed();
AsyncFlowControl flowControl = default;
if (restoreFlow)
{
flowControl = ExecutionContext.SuppressFlow();
}
try
{
if (this.Kind == LockKind.Read)
{
this.continuationTaskScheduler = this.OwningLock.GetTaskSchedulerForReadLockRequest();
}
this.cancellationRegistration = this.cancellationToken.Register(CancellationResponseAction!, this, useSynchronizationContext: false);
this.lck.PendAwaiter(this);
if (this.cancellationToken.IsCancellationRequested && this.cancellationRegistration == default(CancellationTokenRegistration))
{
CancellationResponder(this);
}
}
finally
{
if (restoreFlow)
{
flowControl.Dispose();
}
}
}
}
internal sealed class NonConcurrentSynchronizationContext : SynchronizationContext, IDisposable
{
private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1);
///
/// The managed thread ID of the thread that has entered the semaphore.
///
///
/// No reason to lock around access to this field because it is only ever set to
/// or compared against the current thread, so the activity of other threads is irrelevant.
///
private int? semaphoreHoldingManagedThreadId;
///
/// Gets a value indicating whether the current thread holds the semaphore.
///
private bool IsCurrentThreadHoldingSemaphore
{
get
{
// It is crucial that we capture the field in a local variable to guard against
// the scenario where this thread DOESN'T hold the semaphore but another has, and
// is in the process of clearing it, which would otherwise introduce a race condition
// where we check HasValue to be true, then try to call Value and it ends up throwing.
// Since int? is a value type, copying it to a local value guards against this race
// and we will simply return false in that case since our thread doesn't own it.
int? semaphoreHoldingManagedThreadId = this.semaphoreHoldingManagedThreadId;
return semaphoreHoldingManagedThreadId.HasValue
&& semaphoreHoldingManagedThreadId.Value == Environment.CurrentManagedThreadId;
}
}
public override void Send(SendOrPostCallback d, object? state)
{
throw new NotSupportedException();
}
public override void Post(SendOrPostCallback d, object? state)
{
Requires.NotNull(d, nameof(d));
if (ThreadingEventSource.Instance.IsEnabled())
{
ThreadingEventSource.Instance.PostExecutionStart(d.GetHashCode(), false);
}
// Take special care to minimize allocations and overhead by avoiding implicit delegates and closures.
// The C# compiler caches this delegate in a static field because it never touches "this"
// nor any other local variables, which means the only allocations from this call
// are our Tuple and the ThreadPool's bare-minimum necessary to track the work.
ThreadPool.QueueUserWorkItem(
s =>
{
var tuple = (Tuple)s!;
tuple.Item1.PostHelper(tuple.Item2, tuple.Item3);
},
Tuple.Create(this, d, state));
}
///
public void Dispose()
{
this.semaphore.Dispose();
}
internal LoanBack LoanBackAnyHeldResource(AsyncReaderWriterLock asyncLock)
{
return (this.semaphore.CurrentCount == 0 && this.IsCurrentThreadHoldingSemaphore)
? new LoanBack(this, asyncLock)
: default(LoanBack);
}
internal void EarlyExitSynchronizationContext()
{
if (this.IsCurrentThreadHoldingSemaphore)
{
this.semaphoreHoldingManagedThreadId = null;
this.semaphore.Release();
}
if (SynchronizationContext.Current == this)
{
SynchronizationContext.SetSynchronizationContext(null);
}
}
///
/// Executes the specified delegate.
///
///
/// We use async void instead of async Task because the caller will never
/// use the result, and this way the compiler doesn't have to create the Task object.
///
private async void PostHelper(SendOrPostCallback d, object state)
{
bool delegateInvoked = false;
try
{
await this.semaphore.WaitAsync().ConfigureAwait(false);
this.semaphoreHoldingManagedThreadId = Environment.CurrentManagedThreadId;
try
{
SynchronizationContext.SetSynchronizationContext(this);
if (ThreadingEventSource.Instance.IsEnabled())
{
ThreadingEventSource.Instance.PostExecutionStop(d.GetHashCode());
}
delegateInvoked = true; // set now, before the delegate might throw.
d(state);
}
catch (Exception ex)
{
// We just eat these up to avoid crashing the process by throwing on a threadpool thread.
Report.Fail("An unhandled exception was thrown from within a posted message. {0}", ex);
}
finally
{
// The semaphore *may* have been released already, so take care to not release it again.
if (this.IsCurrentThreadHoldingSemaphore)
{
this.semaphoreHoldingManagedThreadId = null;
this.semaphore.Release();
}
}
}
catch (ObjectDisposedException)
{
// It can happen that this SynchronizationContext was disposed of
// but someone who captured it is still trying to use it.
// In that case, we're not protecting anything any more and we're obliged
// to execute the delegate, so just execute it.
if (!delegateInvoked)
{
SynchronizationContext.SetSynchronizationContext(null);
try
{
delegateInvoked = true; // set now, before the delegate might throw.
d(state);
}
catch (Exception ex)
{
// We just eat these up to avoid crashing the process by throwing on a threadpool thread.
Report.Fail("An unhandled exception was thrown from within a posted message. {0}", ex);
}
}
}
}
internal readonly struct LoanBack : IDisposable
{
private readonly NonConcurrentSynchronizationContext syncContext;
private readonly AsyncReaderWriterLock asyncLock;
internal LoanBack(NonConcurrentSynchronizationContext syncContext, AsyncReaderWriterLock asyncLock)
{
Requires.NotNull(syncContext, nameof(syncContext));
Requires.NotNull(asyncLock, nameof(asyncLock));
this.syncContext = syncContext;
this.asyncLock = asyncLock;
this.syncContext.semaphoreHoldingManagedThreadId = null;
this.syncContext.semaphore.Release();
}
public void Dispose()
{
if (this.syncContext is object)
{
Assumes.False(Monitor.IsEntered(this.asyncLock.syncObject), "Should not wait on the Semaphore, when we hold the syncObject. This causes deadlocks");
this.syncContext.semaphore.Wait();
this.syncContext.semaphoreHoldingManagedThreadId = Environment.CurrentManagedThreadId;
}
}
}
}
internal class EventsHelper
{
private readonly AsyncReaderWriterLock lck;
internal EventsHelper(AsyncReaderWriterLock lck)
{
Requires.NotNull(lck, "lck");
this.lck = lck;
}
internal static void WaitStop(Awaiter lckAwaiter)
{
if (ThreadingEventSource.Instance.IsEnabled())
{
ThreadingEventSource.Instance.WaitReaderWriterLockStop(lckAwaiter.GetHashCode(), lckAwaiter.Kind);
}
}
internal void Issued(Awaiter lckAwaiter)
{
if (ThreadingEventSource.Instance.IsEnabled())
{
ThreadingEventSource.Instance.ReaderWriterLockIssued(lckAwaiter.GetHashCode(), lckAwaiter.Kind, this.lck.issuedUpgradeableReadLocks.Count, this.lck.issuedReadLocks.Count);
}
}
internal void WaitStart(Awaiter lckAwaiter)
{
if (ThreadingEventSource.Instance.IsEnabled())
{
ThreadingEventSource.Instance.WaitReaderWriterLockStart(lckAwaiter.GetHashCode(), lckAwaiter.Kind, this.lck.issuedWriteLocks.Count, this.lck.issuedUpgradeableReadLocks.Count, this.lck.issuedReadLocks.Count);
}
}
}
}
}