private Task ReleaseAsync()

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;
            }
        }