in src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock.cs [1305:1423]
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;
}
}