From 7d23f03e236698092d4eedcc3a3ce6c0752fe47f Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 9 Jul 2026 09:18:55 -0600 Subject: [PATCH] Remove obsolete `TaskCompletionSourceWithoutInlining` class This was originally created before .NET Framework offered the `RunContinuationsAsynchronously` flag. When support for the flag was added, but we still supported downlevel systems, we updated the class to use the flag when supported as a light-up, and keep our old implementation around for downlevel. Then later, we removed the downlevel implementation, but kept the class, which was now just forwarding everything to .NET in every case. So here, I remove the class and redirect all its users to the proper .NET types. --- .../AsyncAutoResetEvent.cs | 6 +- .../AsyncManualResetEvent.cs | 98 ++++--------------- .../AsyncQueue`1.cs | 4 +- .../JoinableTask.cs | 16 +-- .../JoinableTask`1.cs | 8 +- .../TaskCompletionSourceWithoutInlining`1.cs | 63 ------------ .../AsyncManualResetEventTests.cs | 81 +++++++++++++++ 7 files changed, 113 insertions(+), 163 deletions(-) delete mode 100644 src/Microsoft.VisualStudio.Threading/TaskCompletionSourceWithoutInlining`1.cs diff --git a/src/Microsoft.VisualStudio.Threading/AsyncAutoResetEvent.cs b/src/Microsoft.VisualStudio.Threading/AsyncAutoResetEvent.cs index cb4e2e095..aa0d5d6a8 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncAutoResetEvent.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncAutoResetEvent.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -162,7 +160,7 @@ private void OnCancellationRequest(object state) /// /// Tracks someone waiting for a signal from the event. /// - private class WaiterCompletionSource : TaskCompletionSourceWithoutInlining + private class WaiterCompletionSource : TaskCompletionSource { /// /// Initializes a new instance of the class. @@ -171,7 +169,7 @@ private class WaiterCompletionSource : TaskCompletionSourceWithoutInlining to allow continuations to be inlined upon the completer's callstack. /// The cancellation token associated with the waiter. internal WaiterCompletionSource(AsyncAutoResetEvent owner, bool allowInliningContinuations, CancellationToken cancellationToken) - : base(allowInliningContinuations) + : base(allowInliningContinuations ? TaskCreationOptions.None : TaskCreationOptions.RunContinuationsAsynchronously) { this.CancellationToken = cancellationToken; this.Registration = cancellationToken.Register(NullableHelpers.AsNullableArgAction(owner.onCancellationRequestHandler), this); diff --git a/src/Microsoft.VisualStudio.Threading/AsyncManualResetEvent.cs b/src/Microsoft.VisualStudio.Threading/AsyncManualResetEvent.cs index 4bbab53a0..10f081c03 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncManualResetEvent.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncManualResetEvent.cs @@ -2,12 +2,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; -using System.Linq; using System.Runtime.CompilerServices; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -20,9 +17,9 @@ namespace Microsoft.VisualStudio.Threading; public class AsyncManualResetEvent { /// - /// Whether the task completion source should allow executing continuations synchronously. + /// Options to use while creating the to return from . /// - private readonly bool allowInliningAwaiters; + private readonly TaskCreationOptions options; /// /// The object to lock when accessing fields. @@ -36,21 +33,7 @@ public class AsyncManualResetEvent /// This should not need the volatile modifier because it is /// always accessed within a lock. /// - private TaskCompletionSourceWithoutInlining taskCompletionSource; - - /// - /// A flag indicating whether the event is signaled. - /// When this is set to true, it's possible that - /// .Task.IsCompleted is still false - /// if the completion has been scheduled asynchronously. - /// Thus, this field should be the definitive answer as to whether - /// the event is signaled because it is synchronously updated. - /// - /// - /// This should not need the volatile modifier because it is - /// always accessed within a lock. - /// - private bool isSet; + private TaskCompletionSource taskCompletionSource; /// /// Initializes a new instance of the class. @@ -58,17 +41,14 @@ public class AsyncManualResetEvent /// A value indicating whether the event should be initially signaled. /// /// A value indicating whether to allow callers' continuations to execute - /// on the thread that calls before the call returns. - /// callers should not hold private locks if this value is to avoid deadlocks. - /// When , the task returned from may not have fully transitioned to - /// its completed state by the time returns to its caller. + /// on the thread that calls before the call returns. + /// callers should not hold private locks if this value is to avoid deadlocks. /// public AsyncManualResetEvent(bool initialState = false, bool allowInliningAwaiters = false) { - this.allowInliningAwaiters = allowInliningAwaiters; + this.options = allowInliningAwaiters ? TaskCreationOptions.None : TaskCreationOptions.RunContinuationsAsynchronously; - this.taskCompletionSource = this.CreateTaskSource(); - this.isSet = initialState; + this.taskCompletionSource = new(this.options); if (initialState) { this.taskCompletionSource.SetResult(EmptyStruct.Instance); @@ -84,7 +64,7 @@ public bool IsSet { lock (this.syncObject) { - return this.isSet; + return this.taskCompletionSource.Task.IsCompleted; } } } @@ -112,40 +92,21 @@ public Task WaitAsync() /// /// A task that completes when the signal has been set. /// - /// - /// On .NET versions prior to 4.6: - /// This method may return before the signal set has propagated (so may return for a bit more if called immediately). - /// The returned task completes when the signal has definitely been set. - /// - /// - /// On .NET 4.6 and later: /// This method is not asynchronous. The returned Task is always completed. - /// /// [Obsolete("Use Set() instead."), EditorBrowsable(EditorBrowsableState.Never)] public Task SetAsync() { - TaskCompletionSourceWithoutInlining? tcs = null; - bool transitionRequired = false; + TaskCompletionSource? tcs = null; lock (this.syncObject) { - transitionRequired = !this.isSet; tcs = this.taskCompletionSource; - this.isSet = true; } - // Snap the Task that is exposed to the outside so we return that one. - // Once we complete the TaskCompletionSourceWithoutInlinining's task, - // the Task property will return the inner Task. - // SetAsync should return the same Task that WaitAsync callers would have observed previously. - Task result = tcs.Task; - - if (transitionRequired) - { - tcs.TrySetResult(default(EmptyStruct)); - } + tcs.TrySetResult(default); - return result; + // SetAsync should return the same Task that WaitAsync callers would have observed previously. + return tcs.Task; } /// @@ -165,10 +126,9 @@ public void Reset() { lock (this.syncObject) { - if (this.isSet) + if (this.taskCompletionSource.Task.IsCompleted) { - this.taskCompletionSource = this.CreateTaskSource(); - this.isSet = false; + this.taskCompletionSource = new(this.options); } } } @@ -178,20 +138,12 @@ public void Reset() /// /// A task that completes when the signal has been set. /// - /// - /// On .NET versions prior to 4.6: - /// This method may return before the signal set has propagated (so may return for a bit more if called immediately). - /// The returned task completes when the signal has definitely been set. - /// - /// - /// On .NET 4.6 and later: /// This method is not asynchronous. The returned Task is always completed. - /// /// [Obsolete("Use PulseAll() instead."), EditorBrowsable(EditorBrowsableState.Never)] public Task PulseAllAsync() { - TaskCompletionSourceWithoutInlining? tcs = null; + TaskCompletionSource? tcs = null; lock (this.syncObject) { // Atomically replace the completion source with a new, uncompleted source @@ -200,17 +152,13 @@ public Task PulseAllAsync() // continue to return completed Tasks due to a Pulse method which should // execute instantaneously. tcs = this.taskCompletionSource; - this.taskCompletionSource = this.CreateTaskSource(); - this.isSet = false; + this.taskCompletionSource = new(this.options); } - // Snap the Task that is exposed to the outside so we return that one. - // Once we complete the TaskCompletionSourceWithoutInlinining's task, - // the Task property will return the inner Task. + tcs.TrySetResult(default); + // PulseAllAsync should return the same Task that WaitAsync callers would have observed previously. - Task result = tcs.Task; - tcs.TrySetResult(default(EmptyStruct)); - return result; + return tcs.Task; } /// @@ -231,12 +179,4 @@ public TaskAwaiter GetAwaiter() { return this.WaitAsync().GetAwaiter(); } - - /// - /// Creates a new TaskCompletionSource to represent an unset event. - /// - private TaskCompletionSourceWithoutInlining CreateTaskSource() - { - return new TaskCompletionSourceWithoutInlining(this.allowInliningAwaiters); - } } diff --git a/src/Microsoft.VisualStudio.Threading/AsyncQueue`1.cs b/src/Microsoft.VisualStudio.Threading/AsyncQueue`1.cs index 10c7624ba..61affeb04 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncQueue`1.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncQueue`1.cs @@ -5,8 +5,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -298,7 +296,7 @@ public T Peek() this.FreeCanceledDequeuers(); } - var waiterTcs = new TaskCompletionSourceWithoutInlining(allowInliningContinuations: false); + var waiterTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); waiterTcs.AttachCancellation(cancellationToken, this); this.dequeuingWaiters.Enqueue(waiterTcs); return waiterTcs.Task; diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTask.cs b/src/Microsoft.VisualStudio.Threading/JoinableTask.cs index 2cb35d17c..2cf498fdc 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTask.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTask.cs @@ -867,28 +867,28 @@ internal void Post(SendOrPostCallback d, object? state, bool mainThreadAffinitiz } /// - /// Instantiate a that can track the ultimate result of . + /// Instantiate a that can track the ultimate result of . /// /// The new task completion source. /// /// The implementation should be sure to instantiate a that will /// NOT inline continuations, since we'll be completing this ourselves, potentially while holding a private lock. /// - internal virtual object CreateTaskCompletionSource() => new TaskCompletionSourceWithoutInlining(allowInliningContinuations: false); + internal virtual object CreateTaskCompletionSource() => new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); /// - /// Retrieves the from a . + /// Retrieves the from a . /// /// The task completion source. - /// The that will complete with this . - internal virtual Task GetTaskFromCompletionSource(object taskCompletionSource) => ((TaskCompletionSourceWithoutInlining)taskCompletionSource).Task; + /// The that will complete with this . + internal virtual Task GetTaskFromCompletionSource(object taskCompletionSource) => ((TaskCompletionSource)taskCompletionSource).Task; /// - /// Completes a . + /// Completes a . /// /// The task to read a result from. - /// The created earlier with to apply the result to. - internal virtual void CompleteTaskSourceFromWrappedTask(Task wrappedTask, object taskCompletionSource) => wrappedTask.ApplyResultTo((TaskCompletionSourceWithoutInlining)taskCompletionSource); + /// The created earlier with to apply the result to. + internal virtual void CompleteTaskSourceFromWrappedTask(Task wrappedTask, object taskCompletionSource) => wrappedTask.ApplyResultTo((TaskCompletionSource)taskCompletionSource); internal void SetWrappedTask(Task wrappedTask) { diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTask`1.cs b/src/Microsoft.VisualStudio.Threading/JoinableTask`1.cs index c367d5752..9affc39a1 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTask`1.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTask`1.cs @@ -2,12 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Reflection; using System.Runtime.CompilerServices; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -135,10 +131,10 @@ static async Task JoinSlowAsync(JoinableTask me, bool continueOnCapturedCo } /// - internal override object CreateTaskCompletionSource() => new TaskCompletionSourceWithoutInlining(allowInliningContinuations: false); + internal override object CreateTaskCompletionSource() => new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); /// - internal override Task GetTaskFromCompletionSource(object taskCompletionSource) => ((TaskCompletionSourceWithoutInlining)taskCompletionSource).Task; + internal override Task GetTaskFromCompletionSource(object taskCompletionSource) => ((TaskCompletionSource)taskCompletionSource).Task; /// internal override void CompleteTaskSourceFromWrappedTask(Task wrappedTask, object taskCompletionSource) => ((Task)wrappedTask).ApplyResultTo((TaskCompletionSource)taskCompletionSource); diff --git a/src/Microsoft.VisualStudio.Threading/TaskCompletionSourceWithoutInlining`1.cs b/src/Microsoft.VisualStudio.Threading/TaskCompletionSourceWithoutInlining`1.cs deleted file mode 100644 index 5e5002ce2..000000000 --- a/src/Microsoft.VisualStudio.Threading/TaskCompletionSourceWithoutInlining`1.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.VisualStudio.Threading; - -/// -/// A -derivative that -/// does not inline continuations if so configured. -/// -/// The type of the task's resulting value. -internal class TaskCompletionSourceWithoutInlining : TaskCompletionSource -{ - /// - /// The Task that we expose to others that may not inline continuations. - /// - private readonly Task exposedTask; - - /// - /// Initializes a new instance of the class. - /// - /// - /// to allow continuations to be inlined; otherwise . - /// - /// - /// TaskCreationOptions to pass on to the base constructor. - /// - /// The state to set on the Task. - internal TaskCompletionSourceWithoutInlining(bool allowInliningContinuations, TaskCreationOptions options = TaskCreationOptions.None, object? state = null) - : base(state, AdjustFlags(options, allowInliningContinuations)) - { - this.exposedTask = base.Task; - } - - /// - /// Gets the that may never complete inline with completion of this . - /// - /// - /// Return the base.Task if it is already completed since inlining continuations - /// on the completer is no longer a concern. Also, when we are not inlining continuations, - /// this.exposedTask completes slightly later than base.Task, and callers expect - /// the Task we return to be complete as soon as they call TrySetResult. - /// - internal new Task Task => base.Task.IsCompleted ? base.Task : this.exposedTask; - - /// - /// Modifies the specified flags to include RunContinuationsAsynchronously - /// if wanted by the caller and supported by the platform. - /// - /// The base options supplied by the caller. - /// to allow inlining continuations. - /// The possibly modified flags. - private static TaskCreationOptions AdjustFlags(TaskCreationOptions options, bool allowInliningContinuations) - { - return allowInliningContinuations - ? (options & ~TaskCreationOptions.RunContinuationsAsynchronously) - : (options | TaskCreationOptions.RunContinuationsAsynchronously); - } -} diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncManualResetEventTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncManualResetEventTests.cs index 556bc7c20..beaf94e5b 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncManualResetEventTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncManualResetEventTests.cs @@ -59,6 +59,87 @@ public void SetReturnsBeforeInlinedContinuations() Assert.True(inlinedContinuation.Wait(UnexpectedTimeout)); } + /// + /// Verifies that inlining continuations do not delay the Task returned from SetAsync() from completing. + /// + [Fact] + public void SetAsyncReturnsCompletedTaskBeforeNonInlinedContinuationsComplete() + { + ManualResetEventSlim setReturned = new(); + Task? inlinedContinuation = this.evt.WaitAsync() + .ContinueWith( + delegate + { + // Arrange to synchronously block the continuation until released, + // which would deadlock if SetAsync's Task does not complete until inlined continuations complete. + Assert.True(setReturned.Wait(UnexpectedTimeout)); + }, + TaskContinuationOptions.ExecuteSynchronously); +#pragma warning disable CS0618 // Type or member is obsolete + Task setTask = this.evt.SetAsync(); +#pragma warning restore CS0618 // Type or member is obsolete + Assert.True(setTask.IsCompleted); + Assert.True(this.evt.IsSet); + + // Release the continuation so the test doesn't leak a thread. + setReturned.Set(); + + Assert.True(inlinedContinuation.Wait(UnexpectedTimeout)); + } + + /// + /// Verifies that inlining continuations do not delay the Task returned from SetAsync() from completing. + /// + [Fact] + public void SetAsyncAndWaitAsyncReturnsCompletedTaskBeforeInlinedContinuationsComplete() + { + // Reconfigure the object to allow inlining awaiters. + this.evt = new(allowInliningAwaiters: true); + + // Arrange for an awaiter that is not only inlined, but synchronously blocks + // its caller, such that the SetAsync method that completes the Task cannot immediately return. + ManualResetEventSlim continuationStarted = new(); + ManualResetEventSlim setReturned = new(); + Task? inlinedContinuation = this.evt.WaitAsync() + .ContinueWith( + delegate + { + continuationStarted.Set(); + + // Arrange to synchronously block the continuation until released, + // which would deadlock if SetAsync's Task does not complete until inlined continuations complete. + Assert.True(setReturned.Wait(UnexpectedTimeout)); + }, + TaskContinuationOptions.ExecuteSynchronously); + +#pragma warning disable CS0618 // Type or member is obsolete + // First, spin off a thread to call SetAsync. This will be blocked. + Task.Run(() => this.evt.SetAsync(), TestContext.Current.CancellationToken); + + // Now wait for the continuation to start, to verify that SetAsync has done its work, + // even if it can't return to us. + Assert.True(continuationStarted.Wait(UnexpectedTimeout)); + + // Verify that the event is set, even though SetAsync has not yet returned. + Assert.True(this.evt.IsSet); + + // Now call SetAsync again. This time it should return immediately + // even though the first call hasn't yet, and it should return a completed Task. + Task setTask = this.evt.SetAsync(); +#pragma warning restore CS0618 // Type or member is obsolete + + Assert.True(setTask.IsCompleted); + + // Also verify that WaitAsync returns a completed Task, even though the first SetAsync call hasn't yet returned. + Assert.True(this.evt.WaitAsync().IsCompleted); + + // Release the continuation so the test doesn't leak a thread. + setReturned.Set(); + + // Verify that the inlined continuation has completed and didn't throw. + Assert.True(inlinedContinuation.Wait(UnexpectedTimeout)); + } + [Fact] public async Task Blocking() {