diff --git a/src/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterLockTests.cs b/src/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterLockTests.cs index 27fd90873..4cd643daf 100644 --- a/src/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterLockTests.cs +++ b/src/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterLockTests.cs @@ -2509,9 +2509,117 @@ public async Task CancelNonImpactfulToIssuedLocks() Assert.False(this.asyncLock.IsWriteLockHeld); } -#endregion + [StaFact] + public async Task CancelJustBeforeIsCompletedNoLeak() + { + var lockAwaitFinished = new TaskCompletionSource(); + var testCompleted = new TaskCompletionSource(); + var cts = new CancellationTokenSource(); + + Thread staThread = new Thread((ThreadStart)delegate + { + try + { + var awaitable = this.asyncLock.UpgradeableReadLockAsync(cts.Token); + var awaiter = awaitable.GetAwaiter(); + cts.Cancel(); + + if (awaiter.IsCompleted) + { + try + { + awaiter.GetResult().Dispose(); + Assert.True(false, "The lock should not be issued on an STA thread."); + } + catch (OperationCanceledException) + { + } + + lockAwaitFinished.SetAsync(); + } + else + { + awaiter.OnCompleted(delegate + { + Assert.Equal(ApartmentState.MTA, Thread.CurrentThread.GetApartmentState()); + try + { + awaiter.GetResult().Dispose(); + } + catch (OperationCanceledException) + { + } + + lockAwaitFinished.SetAsync(); + }); + } + + lockAwaitFinished.Task.Wait(); + + // No lock is leaked + awaitable = this.asyncLock.UpgradeableReadLockAsync(); + awaiter = awaitable.GetAwaiter(); + Assert.False(awaiter.IsCompleted, "The lock should not be issued on an STA thread."); + awaiter.OnCompleted(delegate + { + Assert.Equal(ApartmentState.MTA, Thread.CurrentThread.GetApartmentState()); + awaiter.GetResult().Dispose(); + testCompleted.SetAsync(); + }); + } + catch (Exception ex) + { + testCompleted.TrySetException(ex); + } + }); + + staThread.SetApartmentState(ApartmentState.STA); + staThread.Start(); + await testCompleted.Task; + } + + [StaFact] + public async Task CancelJustAfterIsCompleted() + { + var lockAwaitFinished = new TaskCompletionSource(); + var testCompleted = new TaskCompletionSource(); + var readlockTask = Task.Run(async delegate + { + using (await this.asyncLock.ReadLockAsync()) + { + await lockAwaitFinished.SetAsync(); + await testCompleted.Task; + } + }); + + await lockAwaitFinished.Task; + + var cts = new CancellationTokenSource(); + + var awaitable = this.asyncLock.WriteLockAsync(cts.Token); + var awaiter = awaitable.GetAwaiter(); + Assert.False(awaiter.IsCompleted, "The lock should not be issued until read lock is issued."); + + cts.Cancel(); + awaiter.OnCompleted(delegate + { + try + { + awaiter.GetResult().Dispose(); + } + catch (OperationCanceledException) + { + } + + testCompleted.SetAsync(); + }); + + await readlockTask; + } + + #endregion -#region Completion tests + #region Completion tests #if DESKTOP || NETCOREAPP2_0 [StaFact] diff --git a/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock.cs b/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock.cs index b11543574..c609925ae 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock.cs @@ -2160,7 +2160,21 @@ internal Awaiter(AsyncReaderWriterLock lck, LockKind kind, LockFlags options, Ca /// public bool IsCompleted { - get { return this.cancellationToken.IsCancellationRequested || this.fault != null || this.LockIssued; } + get + { + if (this.fault != null) + { + 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; + } } /// @@ -2270,8 +2284,8 @@ public void OnCompleted(Action continuation) throw new NotSupportedException("Multiple continuations are not supported."); } - this.cancellationRegistration = this.cancellationToken.Register(CancellationResponseAction, this, useSynchronizationContext: false); this.lck.PendAwaiter(this); + this.cancellationRegistration = this.cancellationToken.Register(CancellationResponseAction, this, useSynchronizationContext: false); } ///