/// <inheritdoc /> public override async Task ExecuteAsync(Func <Task> operation, CancellationToken cancellationToken = default) { Requires.NotNull(operation, nameof(operation)); this.ThrowIfFaulted(); StrongBox <bool> ownedBox = this.reentrancyDetection.Value; if (ownedBox?.Value ?? false) { throw Verify.FailOperation("Semaphore is already held and reentrancy setting is '{0}'.", ReentrancyMode.NotAllowed); } await this.ExecuteCoreAsync(async delegate { using (this.joinableTaskCollection?.Join()) { AsyncSemaphore.Releaser releaser = await this.semaphore.EnterAsync(cancellationToken).ConfigureAwait(true); try { this.reentrancyDetection.Value = ownedBox = new StrongBox <bool>(true); await operation().ConfigureAwaitRunInline(); } finally { // Make it clear to any forks of our ExecutionContexxt that the semaphore is no longer owned. ownedBox.Value = false; DisposeReleaserNoThrow(releaser); } } }); }
/// <summary> /// Disposes the specfied release, swallowing certain exceptions. /// </summary> /// <param name="releaser">The releaser to dispose.</param> private static void DisposeReleaserNoThrow(AsyncSemaphore.Releaser releaser) { try { releaser.Dispose(); } catch (ObjectDisposedException) { // Swallow this, since in releasing the semaphore if it's already disposed the caller probably doesn't care. } }
/// <inheritdoc /> public override async Task ExecuteAsync(Func <Task> operation, CancellationToken cancellationToken = default) { Requires.NotNull(operation, nameof(operation)); this.ThrowIfFaulted(); // No race condition here: We're accessing AsyncLocal<T> which we by definition have our own copy of. // Multiple threads or multiple async methods will all have their own storage for this field. Stack <StrongBox <AsyncSemaphore.Releaser> > reentrantStack = this.reentrantCount.Value; if (reentrantStack == null || reentrantStack.Count == 0) { // When the stack is empty, the semaphore isn't held. But many execution contexts that forked from a common root // would be sharing this same empty Stack<T> instance. If we pushed to that Stack, all those forks would suddenly // be seen as having entered this new top-level semaphore. We therefore allocate a new Stack and assign it to our // AsyncLocal<T> field so that only this particular ExecutionContext is seen as having entered the semaphore. this.reentrantCount.Value = reentrantStack = new Stack <StrongBox <AsyncSemaphore.Releaser> >(capacity: 2); } await this.ExecuteCoreAsync(async delegate { using (this.joinableTaskCollection?.Join()) { AsyncSemaphore.Releaser releaser = reentrantStack.Count == 0 ? await this.semaphore.EnterAsync(cancellationToken).ConfigureAwait(true) : default; var pushedReleaser = new StrongBox <AsyncSemaphore.Releaser>(releaser); lock (reentrantStack) { reentrantStack.Push(pushedReleaser); } try { await operation().ConfigureAwaitRunInline(); } finally { lock (reentrantStack) { var poppedReleaser = reentrantStack.Pop(); if (!object.ReferenceEquals(poppedReleaser, pushedReleaser)) { this.faulted = true; throw Verify.FailOperation(Strings.SemaphoreStackNestingViolated, ReentrancyMode.Stack); } } DisposeReleaserNoThrow(releaser); } } });