diff --git a/src/mscorlib/shared/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs b/src/mscorlib/shared/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs index 4ec931c4af31..0784e6135c83 100644 --- a/src/mscorlib/shared/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs +++ b/src/mscorlib/shared/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs @@ -39,7 +39,7 @@ public struct ConfiguredValueTaskAwaiter : ICriticalNotifyCompletion /// The value being awaited. private ValueTask _value; // Methods are called on this; avoid making it readonly so as to avoid unnecessary copies /// The value to pass to ConfigureAwait. - private readonly bool _continueOnCapturedContext; + internal readonly bool _continueOnCapturedContext; /// Initializes the awaiter. /// The value to be awaited. @@ -66,6 +66,9 @@ public void OnCompleted(Action continuation) => /// Schedules the continuation action for the . public void UnsafeOnCompleted(Action continuation) => _value.AsTask().ConfigureAwait(_continueOnCapturedContext).GetAwaiter().UnsafeOnCompleted(continuation); + + /// Gets the task underlying . + internal Task AsTask() => _value.AsTask(); } } } diff --git a/src/mscorlib/shared/System/Runtime/CompilerServices/ValueTaskAwaiter.cs b/src/mscorlib/shared/System/Runtime/CompilerServices/ValueTaskAwaiter.cs index c4194825217a..30e688e07711 100644 --- a/src/mscorlib/shared/System/Runtime/CompilerServices/ValueTaskAwaiter.cs +++ b/src/mscorlib/shared/System/Runtime/CompilerServices/ValueTaskAwaiter.cs @@ -33,5 +33,8 @@ public void OnCompleted(Action continuation) => /// Schedules the continuation action for this ValueTask. public void UnsafeOnCompleted(Action continuation) => _value.AsTask().ConfigureAwait(continueOnCapturedContext: true).GetAwaiter().UnsafeOnCompleted(continuation); + + /// Gets the task underlying . + internal Task AsTask() => _value.AsTask(); } } diff --git a/src/mscorlib/src/System/Runtime/CompilerServices/AsyncMethodBuilder.cs b/src/mscorlib/src/System/Runtime/CompilerServices/AsyncMethodBuilder.cs index 704628690a48..e8e0d60202f1 100644 --- a/src/mscorlib/src/System/Runtime/CompilerServices/AsyncMethodBuilder.cs +++ b/src/mscorlib/src/System/Runtime/CompilerServices/AsyncMethodBuilder.cs @@ -365,7 +365,7 @@ public void AwaitOnCompleted( { try { - awaiter.OnCompleted(GetMoveNextDelegate(ref stateMachine)); + awaiter.OnCompleted(GetStateMachineBox(ref stateMachine).MoveNextAction); } catch (Exception e) { @@ -384,10 +384,107 @@ public void AwaitUnsafeOnCompleted( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine + { + IAsyncStateMachineBox box = GetStateMachineBox(ref stateMachine); + + // TODO https://github.com/dotnet/coreclr/issues/12877: + // Once the JIT is able to recognize "awaiter is ITaskAwaiter" and "awaiter is IConfiguredTaskAwaiter", + // use those in order to a) consolidate a lot of this code, and b) handle all Task/Task and not just + // the few types special-cased here. For now, handle common {Configured}TaskAwaiter. Having the types + // explicitly listed here allows the JIT to generate the best code for them; otherwise we'll fall through + // to the later workaround. + if (typeof(TAwaiter) == typeof(TaskAwaiter) || + typeof(TAwaiter) == typeof(TaskAwaiter) || + typeof(TAwaiter) == typeof(TaskAwaiter) || + typeof(TAwaiter) == typeof(TaskAwaiter) || + typeof(TAwaiter) == typeof(TaskAwaiter) || + typeof(TAwaiter) == typeof(TaskAwaiter) || + typeof(TAwaiter) == typeof(TaskAwaiter) || + typeof(TAwaiter) == typeof(TaskAwaiter)) + { + ref TaskAwaiter ta = ref Unsafe.As(ref awaiter); // relies on TaskAwaiter/TaskAwaiter having the same layout + TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, continueOnCapturedContext: true); + } + else if ( + typeof(TAwaiter) == typeof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter) || + typeof(TAwaiter) == typeof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter) || + typeof(TAwaiter) == typeof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter) || + typeof(TAwaiter) == typeof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter) || + typeof(TAwaiter) == typeof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter) || + typeof(TAwaiter) == typeof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter) || + typeof(TAwaiter) == typeof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter) || + typeof(TAwaiter) == typeof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter)) + { + ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter ta = ref Unsafe.As(ref awaiter); + TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, ta.m_continueOnCapturedContext); + } + + // Handle common {Configured}ValueTaskAwaiter types. Unfortunately these need to be special-cased + // individually, as we don't have good way to extract the task from a ValueTaskAwaiter when we don't + // know what the T is; we could make ValueTaskAwaiter implement an IValueTaskAwaiter interface, but + // calling a GetTask method on that would end up boxing the awaiter. This hard-coded list here is + // somewhat arbitrary and is based on types currently in use with ValueTask in coreclr/corefx. + else if (typeof(TAwaiter) == typeof(ValueTaskAwaiter)) + { + var vta = (ValueTaskAwaiter)(object)awaiter; + TaskAwaiter.UnsafeOnCompletedInternal(vta.AsTask(), box, continueOnCapturedContext: true); + } + else if (typeof(TAwaiter) == typeof(ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter)) + { + var vta = (ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter)(object)awaiter; + TaskAwaiter.UnsafeOnCompletedInternal(vta.AsTask(), box, vta._continueOnCapturedContext); + } + else if (typeof(TAwaiter) == typeof(ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter)) + { + var vta = (ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter)(object)awaiter; + TaskAwaiter.UnsafeOnCompletedInternal(vta.AsTask(), box, vta._continueOnCapturedContext); + } + else if (typeof(TAwaiter) == typeof(ConfiguredValueTaskAwaitable>.ConfiguredValueTaskAwaiter)) + { + var vta = (ConfiguredValueTaskAwaitable>.ConfiguredValueTaskAwaiter)(object)awaiter; + TaskAwaiter.UnsafeOnCompletedInternal(vta.AsTask(), box, vta._continueOnCapturedContext); + } + else if (typeof(TAwaiter) == typeof(ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter)) + { + var vta = (ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter)(object)awaiter; + TaskAwaiter.UnsafeOnCompletedInternal(vta.AsTask(), box, vta._continueOnCapturedContext); + } + + // To catch all Task/Task awaits, do the currently more expensive interface checks. + // Eventually these and the above Task/Task checks should be replaced by "is" checks, + // once that's recognized and optimized by the JIT. We do these after all of the hardcoded + // checks above so that they don't incur the costs of these checks. + else if (InterfaceIsCheckWorkaround.IsITaskAwaiter) + { + ref TaskAwaiter ta = ref Unsafe.As(ref awaiter); + TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, continueOnCapturedContext: true); + } + else if (InterfaceIsCheckWorkaround.IsIConfiguredTaskAwaiter) + { + ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter ta = ref Unsafe.As(ref awaiter); + TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, ta.m_continueOnCapturedContext); + } + + // The awaiter isn't specially known. Fall back to doing a normal await. + else + { + // TODO https://github.com/dotnet/coreclr/issues/14177: + // Move the code back into this method once the JIT is able to + // elide it successfully when one of the previous branches is hit. + AwaitArbitraryAwaiterUnsafeOnCompleted(ref awaiter, box); + } + } + + /// Schedules the specified state machine to be pushed forward when the specified awaiter completes. + /// Specifies the type of the awaiter. + /// The awaiter. + /// The state machine box. + private static void AwaitArbitraryAwaiterUnsafeOnCompleted(ref TAwaiter awaiter, IAsyncStateMachineBox box) + where TAwaiter : ICriticalNotifyCompletion { try { - awaiter.UnsafeOnCompleted(GetMoveNextDelegate(ref stateMachine)); + awaiter.UnsafeOnCompleted(box.MoveNextAction); } catch (Exception e) { @@ -399,7 +496,7 @@ public void AwaitUnsafeOnCompleted( /// Specifies the type of the async state machine. /// The state machine. /// The "boxed" state machine. - private Action GetMoveNextDelegate( + private IAsyncStateMachineBox GetStateMachineBox( ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { @@ -416,7 +513,7 @@ private Action GetMoveNextDelegate( { stronglyTypedBox.Context = currentContext; } - return stronglyTypedBox.MoveNextAction; + return stronglyTypedBox; } // The least common case: we have a weakly-typed boxed. This results if the debugger @@ -440,7 +537,7 @@ private Action GetMoveNextDelegate( // Update the context. This only happens with a debugger, so no need to spend // extra IL checking for equality before doing the assignment. weaklyTypedBox.Context = currentContext; - return weaklyTypedBox.MoveNextAction; + return weaklyTypedBox; } // Alert a listening debugger that we can't make forward progress unless it slips threads. @@ -462,34 +559,33 @@ private Action GetMoveNextDelegate( m_task = box; // important: this must be done before storing stateMachine into box.StateMachine! box.StateMachine = stateMachine; box.Context = currentContext; - return box.MoveNextAction; + return box; } /// A strongly-typed box for Task-based async state machines. /// Specifies the type of the state machine. /// Specifies the type of the Task's result. private sealed class AsyncStateMachineBox : - Task, IDebuggingAsyncStateMachineAccessor + Task, IAsyncStateMachineBox where TStateMachine : IAsyncStateMachine { /// Delegate used to invoke on an ExecutionContext when passed an instance of this box type. private static readonly ContextCallback s_callback = s => ((AsyncStateMachineBox)s).StateMachine.MoveNext(); /// A delegate to the method. - public readonly Action MoveNextAction; + private Action _moveNextAction; /// The state machine itself. public TStateMachine StateMachine; // mutable struct; do not make this readonly /// Captured ExecutionContext with which to invoke ; may be null. public ExecutionContext Context; - public AsyncStateMachineBox() - { - var mn = new Action(MoveNext); - MoveNextAction = AsyncCausalityTracer.LoggingOn ? AsyncMethodBuilderCore.OutputAsyncCausalityEvents(this, mn) : mn; - } + /// A delegate to the method. + public Action MoveNextAction => + _moveNextAction ?? + (_moveNextAction = AsyncCausalityTracer.LoggingOn ? AsyncMethodBuilderCore.OutputAsyncCausalityEvents(this, new Action(MoveNext)) : new Action(MoveNext)); - /// Call MoveNext on . - private void MoveNext() + /// Calls MoveNext on + public void MoveNext() { if (Context == null) { @@ -501,8 +597,19 @@ private void MoveNext() } } + /// + /// Calls MoveNext on . Implements ITaskCompletionAction.Invoke so + /// that the state machine object may be queued directly as a continuation into a Task's + /// continuation slot/list. + /// + /// The completing task that caused this method to be invoked, if there was one. + void ITaskCompletionAction.Invoke(Task completedTask) => MoveNext(); + + /// Signals to Task's continuation logic that runs arbitrary user code via MoveNext. + bool ITaskCompletionAction.InvokeMayRunArbitraryCode => true; + /// Gets the state machine as a boxed object. This should only be used for debugging purposes. - IAsyncStateMachine IDebuggingAsyncStateMachineAccessor.GetStateMachineObject() => StateMachine; // likely boxes, only use for debugging + IAsyncStateMachine IAsyncStateMachineBox.GetStateMachineObject() => StateMachine; // likely boxes, only use for debugging } /// Gets the for this builder. @@ -815,12 +922,24 @@ internal static Task CreateCacheableTask(TResult result) => new Task(false, result, (TaskCreationOptions)InternalTaskOptions.DoNotDispose, default(CancellationToken)); } + /// Temporary workaround for https://github.com/dotnet/coreclr/issues/12877. + internal static class InterfaceIsCheckWorkaround + { + internal static readonly bool IsITaskAwaiter = typeof(TAwaiter).GetInterface("ITaskAwaiter") != null; + internal static readonly bool IsIConfiguredTaskAwaiter = typeof(TAwaiter).GetInterface("IConfiguredTaskAwaiter") != null; + } + /// - /// An interface implemented by to allow access - /// non-generically to state associated with a builder and state machine. + /// An interface implemented by all instances, regardless of generics. /// - interface IDebuggingAsyncStateMachineAccessor + interface IAsyncStateMachineBox : ITaskCompletionAction { + /// + /// Gets an action for moving forward the contained state machine. + /// This will lazily-allocate the delegate as needed. + /// + Action MoveNextAction { get; } + /// Gets the state machine as a boxed object. This should only be used for debugging purposes. IAsyncStateMachine GetStateMachineObject(); } @@ -843,7 +962,7 @@ internal static Action TryGetStateMachineForDebugger(Action action) // debugger { object target = action.Target; return - target is IDebuggingAsyncStateMachineAccessor sm ? sm.GetStateMachineObject().MoveNext : + target is IAsyncStateMachineBox sm ? sm.GetStateMachineObject().MoveNext : target is ContinuationWrapper cw ? TryGetStateMachineForDebugger(cw._continuation) : action; } diff --git a/src/mscorlib/src/System/Runtime/CompilerServices/TaskAwaiter.cs b/src/mscorlib/src/System/Runtime/CompilerServices/TaskAwaiter.cs index 93bedd62b6f3..ce8867773037 100644 --- a/src/mscorlib/src/System/Runtime/CompilerServices/TaskAwaiter.cs +++ b/src/mscorlib/src/System/Runtime/CompilerServices/TaskAwaiter.cs @@ -56,10 +56,13 @@ namespace System.Runtime.CompilerServices { /// Provides an awaiter for awaiting a . /// This type is intended for compiler use only. - public struct TaskAwaiter : ICriticalNotifyCompletion + public struct TaskAwaiter : ICriticalNotifyCompletion, ITaskAwaiter { + // WARNING: Unsafe.As is used to access the generic TaskAwaiter<> as TaskAwaiter. + // Its layout must remain the same. + /// The task being awaited. - private readonly Task m_task; + internal readonly Task m_task; /// Initializes the . /// The to be awaited. @@ -212,6 +215,30 @@ internal static void OnCompletedInternal(Task task, Action continuation, bool co task.SetContinuationForAwait(continuation, continueOnCapturedContext, flowExecutionContext); } + /// Schedules the continuation onto the associated with this . + /// The task being awaited. + /// The action to invoke when the await operation completes. + /// Whether to capture and marshal back to the current context. + /// Whether to flow ExecutionContext across the await. + /// The argument is null (Nothing in Visual Basic). + /// The awaiter was not properly initialized. + /// This method is intended for compiler user rather than use directly in code. + internal static void UnsafeOnCompletedInternal(Task task, IAsyncStateMachineBox stateMachineBox, bool continueOnCapturedContext) + { + Debug.Assert(stateMachineBox != null); + + // If TaskWait* ETW events are enabled, trace a beginning event for this await + // and set up an ending event to be traced when the asynchronous await completes. + if (TplEtwProvider.Log.IsEnabled() || Task.s_asyncDebuggingEnabled) + { + task.SetContinuationForAwait(OutputWaitEtwEvents(task, stateMachineBox.MoveNextAction), continueOnCapturedContext, flowExecutionContext: false); + } + else + { + task.UnsafeSetContinuationForAwait(stateMachineBox, continueOnCapturedContext); + } + } + /// /// Outputs a WaitBegin ETW event, and augments the continuation action to output a WaitEnd ETW event. /// @@ -290,8 +317,11 @@ private static Action OutputWaitEtwEvents(Task task, Action continuation) /// Provides an awaiter for awaiting a . /// This type is intended for compiler use only. - public struct TaskAwaiter : ICriticalNotifyCompletion + public struct TaskAwaiter : ICriticalNotifyCompletion, ITaskAwaiter { + // WARNING: Unsafe.As is used to access TaskAwaiter<> as the non-generic TaskAwaiter. + // Its layout must remain the same. + /// The task being awaited. private readonly Task m_task; @@ -343,6 +373,20 @@ public TResult GetResult() } } + /// + /// Marker interface used to know whether a particular awaiter is either a + /// TaskAwaiter or a TaskAwaiter`1. It must not be implemented by any other + /// awaiters. + /// + internal interface ITaskAwaiter { } + + /// + /// Marker interface used to know whether a particular awaiter is either a + /// CTA.ConfiguredTaskAwaiter or a CTA`1.ConfiguredTaskAwaiter. It must not + /// be implemented by any other awaiters. + /// + internal interface IConfiguredTaskAwaiter { } + /// Provides an awaitable object that allows for configured awaits on . /// This type is intended for compiler use only. public struct ConfiguredTaskAwaitable @@ -370,12 +414,15 @@ public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter() /// Provides an awaiter for a . /// This type is intended for compiler use only. - public struct ConfiguredTaskAwaiter : ICriticalNotifyCompletion + public struct ConfiguredTaskAwaiter : ICriticalNotifyCompletion, IConfiguredTaskAwaiter { + // WARNING: Unsafe.As is used to access the generic ConfiguredTaskAwaiter as this. + // Its layout must remain the same. + /// The task being awaited. - private readonly Task m_task; + internal readonly Task m_task; /// Whether to attempt marshaling back to the original context. - private readonly bool m_continueOnCapturedContext; + internal readonly bool m_continueOnCapturedContext; /// Initializes the . /// The to await. @@ -456,8 +503,11 @@ public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter() /// Provides an awaiter for a . /// This type is intended for compiler use only. - public struct ConfiguredTaskAwaiter : ICriticalNotifyCompletion + public struct ConfiguredTaskAwaiter : ICriticalNotifyCompletion, IConfiguredTaskAwaiter { + // WARNING: Unsafe.As is used to access this as the non-generic ConfiguredTaskAwaiter. + // Its layout must remain the same. + /// The task being awaited. private readonly Task m_task; /// Whether to attempt marshaling back to the original context. diff --git a/src/mscorlib/src/System/Threading/Tasks/Task.cs b/src/mscorlib/src/System/Threading/Tasks/Task.cs index 84811fb06835..732f26141c3d 100644 --- a/src/mscorlib/src/System/Threading/Tasks/Task.cs +++ b/src/mscorlib/src/System/Threading/Tasks/Task.cs @@ -2627,6 +2627,57 @@ internal void SetContinuationForAwait( } } + /// + /// Sets a continuation onto the . + /// The continuation is scheduled to run in the current synchronization context is one exists, + /// otherwise in the current task scheduler. + /// + /// The action to invoke when the has completed. + /// + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. + /// + /// The awaiter was not properly initialized. + internal void UnsafeSetContinuationForAwait(IAsyncStateMachineBox stateMachineBox, bool continueOnCapturedContext) + { + Debug.Assert(stateMachineBox != null); + + // If the caller wants to continue on the current context/scheduler and there is one, + // fall back to using the state machine's delegate. + if (continueOnCapturedContext) + { + SynchronizationContext syncCtx = SynchronizationContext.CurrentNoFlow; + if (syncCtx != null && syncCtx.GetType() != typeof(SynchronizationContext)) + { + Action moveNextAction = stateMachineBox.MoveNextAction; + if (!AddTaskContinuation(new SynchronizationContextAwaitTaskContinuation(syncCtx, moveNextAction, flowExecutionContext: false), addBeforeOthers: false)) + { + AwaitTaskContinuation.UnsafeScheduleAction(moveNextAction, this); + } + return; + } + else + { + TaskScheduler scheduler = TaskScheduler.InternalCurrent; + if (scheduler != null && scheduler != TaskScheduler.Default) + { + Action moveNextAction = stateMachineBox.MoveNextAction; + if (!AddTaskContinuation(new TaskSchedulerAwaitTaskContinuation(scheduler, moveNextAction, flowExecutionContext: false), addBeforeOthers: false)) + { + AwaitTaskContinuation.UnsafeScheduleAction(moveNextAction, this); + } + return; + } + } + } + + // Otherwise, add the state machine box directly as the ITaskCompletionAction continuation. + // If we're unable to because the task has already completed, queue the delegate. + if (!AddTaskContinuation(stateMachineBox, addBeforeOthers: false)) + { + AwaitTaskContinuation.UnsafeScheduleAction(stateMachineBox.MoveNextAction, this); + } + } + /// Creates an awaitable that asynchronously yields back to the current context when awaited. /// /// A context that, when awaited, will asynchronously transition back into the current context at the