diff --git a/src/mscorlib/src/System/Runtime/CompilerServices/AsyncMethodBuilder.cs b/src/mscorlib/src/System/Runtime/CompilerServices/AsyncMethodBuilder.cs index fc7a7f01a31d..c854f608b35f 100644 --- a/src/mscorlib/src/System/Runtime/CompilerServices/AsyncMethodBuilder.cs +++ b/src/mscorlib/src/System/Runtime/CompilerServices/AsyncMethodBuilder.cs @@ -10,17 +10,12 @@ // // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Runtime.ExceptionServices; -using System.Security; using System.Threading; using System.Threading.Tasks; - #if FEATURE_COMINTEROP using System.Runtime.InteropServices.WindowsRuntime; #endif // FEATURE_COMINTEROP @@ -34,23 +29,17 @@ namespace System.Runtime.CompilerServices public struct AsyncVoidMethodBuilder { /// The synchronization context associated with this operation. - private SynchronizationContext m_synchronizationContext; - /// State related to the IAsyncStateMachine. - private AsyncMethodBuilderCore m_coreState; // mutable struct: must not be readonly - /// Task used for debugging and logging purposes only. Lazily initialized. - private Task m_task; + private SynchronizationContext _synchronizationContext; + /// The builder this void builder wraps. + private AsyncTaskMethodBuilder _builder; // mutable struct: must not be readonly /// Initializes a new . /// The initialized . public static AsyncVoidMethodBuilder Create() { - // Capture the current sync context. If there isn't one, use the dummy s_noContextCaptured - // instance; this allows us to tell the state of no captured context apart from the state - // of an improperly constructed builder instance. SynchronizationContext sc = SynchronizationContext.CurrentNoFlow; - if (sc != null) - sc.OperationStarted(); - return new AsyncVoidMethodBuilder() { m_synchronizationContext = sc }; + sc?.OperationStarted(); + return new AsyncVoidMethodBuilder() { _synchronizationContext = sc }; } /// Initiates the builder's execution with the associated state machine. @@ -58,39 +47,15 @@ public static AsyncVoidMethodBuilder Create() /// The state machine instance, passed by reference. /// The argument was null (Nothing in Visual Basic). [DebuggerStepThrough] - public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine - { - // See comment on AsyncMethodBuilderCore.Start - // AsyncMethodBuilderCore.Start(ref stateMachine); - - if (stateMachine == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine); - Contract.EndContractBlock(); - - // Run the MoveNext method within a copy-on-write ExecutionContext scope. - // This allows us to undo any ExecutionContext changes made in MoveNext, - // so that they won't "leak" out of the first await. - - Thread currentThread = Thread.CurrentThread; - ExecutionContextSwitcher ecs = default(ExecutionContextSwitcher); - try - { - ExecutionContext.EstablishCopyOnWriteScope(currentThread, ref ecs); - stateMachine.MoveNext(); - } - finally - { - ecs.Undo(currentThread); - } - } + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => + _builder.Start(ref stateMachine); /// Associates the builder with the state machine it represents. /// The heap-allocated state machine object. /// The argument was null (Nothing in Visual Basic). /// The builder is incorrectly initialized. - public void SetStateMachine(IAsyncStateMachine stateMachine) - { - m_coreState.SetStateMachine(stateMachine); // argument validation handled by AsyncMethodBuilderCore - } + public void SetStateMachine(IAsyncStateMachine stateMachine) => + _builder.SetStateMachine(stateMachine); /// /// Schedules the specified state machine to be pushed forward when the specified awaiter completes. @@ -102,41 +67,8 @@ public void SetStateMachine(IAsyncStateMachine stateMachine) public void AwaitOnCompleted( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion - where TStateMachine : IAsyncStateMachine - { - try - { - AsyncMethodBuilderCore.MoveNextRunner runnerToInitialize = null; - var continuation = m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn ? this.Task : null, ref runnerToInitialize); - Debug.Assert(continuation != null, "GetCompletionAction should always return a valid action."); - - // If this is our first await, such that we've not yet boxed the state machine, do so now. - if (m_coreState.m_stateMachine == null) - { - if (AsyncCausalityTracer.LoggingOn) - AsyncCausalityTracer.TraceOperationCreation(CausalityTraceLevel.Required, this.Task.Id, "Async: " + stateMachine.GetType().Name, 0); - - // Box the state machine, then tell the boxed instance to call back into its own builder, - // so we can cache the boxed reference. NOTE: The language compiler may choose to use - // a class instead of a struct for the state machine for debugging purposes; in such cases, - // the stateMachine will already be an object. - m_coreState.PostBoxInitialization(stateMachine, runnerToInitialize, null); - } - - awaiter.OnCompleted(continuation); - } - catch (Exception exc) - { - // Prevent exceptions from leaking to the call site, which could - // then allow multiple flows of execution through the same async method - // if the awaiter had already scheduled the continuation by the time - // the exception was thrown. We propagate the exception on the - // ThreadPool because we can trust it to not throw, unlike - // if we were to go to a user-supplied SynchronizationContext, - // whose Post method could easily throw. - AsyncMethodBuilderCore.ThrowAsync(exc, targetContext: null); - } - } + where TStateMachine : IAsyncStateMachine => + _builder.AwaitOnCompleted(ref awaiter, ref stateMachine); /// /// Schedules the specified state machine to be pushed forward when the specified awaiter completes. @@ -148,45 +80,23 @@ public void AwaitOnCompleted( public void AwaitUnsafeOnCompleted( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine - { - try - { - AsyncMethodBuilderCore.MoveNextRunner runnerToInitialize = null; - var continuation = m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn ? this.Task : null, ref runnerToInitialize); - Debug.Assert(continuation != null, "GetCompletionAction should always return a valid action."); - - // If this is our first await, such that we've not yet boxed the state machine, do so now. - if (m_coreState.m_stateMachine == null) - { - if (AsyncCausalityTracer.LoggingOn) - AsyncCausalityTracer.TraceOperationCreation(CausalityTraceLevel.Required, this.Task.Id, "Async: " + stateMachine.GetType().Name, 0); - - // Box the state machine, then tell the boxed instance to call back into its own builder, - // so we can cache the boxed reference. NOTE: The language compiler may choose to use - // a class instead of a struct for the state machine for debugging purposes; in such cases, - // the stateMachine will already be an object. - m_coreState.PostBoxInitialization(stateMachine, runnerToInitialize, null); - } - - awaiter.UnsafeOnCompleted(continuation); - } - catch (Exception e) - { - AsyncMethodBuilderCore.ThrowAsync(e, targetContext: null); - } - } + where TStateMachine : IAsyncStateMachine => + _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); /// Completes the method builder successfully. public void SetResult() { if (AsyncCausalityTracer.LoggingOn) + { AsyncCausalityTracer.TraceOperationCompletion(CausalityTraceLevel.Required, this.Task.Id, AsyncCausalityStatus.Completed); + } - if (m_synchronizationContext != null) + if (_synchronizationContext != null) { NotifySynchronizationContextOfCompletion(); } + + // No need to call _builder.SetResult, as no one pays attention to the task's completion. } /// Faults the method builder with an exception. @@ -195,19 +105,23 @@ public void SetResult() /// The builder is not initialized. public void SetException(Exception exception) { - if (exception == null) throw new ArgumentNullException(nameof(exception)); - Contract.EndContractBlock(); + if (exception == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.exception); + } if (AsyncCausalityTracer.LoggingOn) + { AsyncCausalityTracer.TraceOperationCompletion(CausalityTraceLevel.Required, this.Task.Id, AsyncCausalityStatus.Error); + } - if (m_synchronizationContext != null) + if (_synchronizationContext != null) { // If we captured a synchronization context, Post the throwing of the exception to it // and decrement its outstanding operation count. try { - AsyncMethodBuilderCore.ThrowAsync(exception, targetContext: m_synchronizationContext); + AsyncStateMachineHelpers.ThrowAsync(exception, targetContext: _synchronizationContext); } finally { @@ -219,28 +133,30 @@ public void SetException(Exception exception) // Otherwise, queue the exception to be thrown on the ThreadPool. This will // result in a crash unless legacy exception behavior is enabled by a config // file or a CLR host. - AsyncMethodBuilderCore.ThrowAsync(exception, targetContext: null); + AsyncStateMachineHelpers.ThrowAsync(exception, targetContext: null); } + + // No need to call _builder.SetException, as no one pays attention to the task's completion. } /// Notifies the current synchronization context that the operation completed. private void NotifySynchronizationContextOfCompletion() { - Debug.Assert(m_synchronizationContext != null, "Must only be used with a non-null context."); + Debug.Assert(_synchronizationContext != null, "Must only be used with a non-null context."); try { - m_synchronizationContext.OperationCompleted(); + _synchronizationContext.OperationCompleted(); } catch (Exception exc) { // If the interaction with the SynchronizationContext goes awry, // fall back to propagating on the ThreadPool. - AsyncMethodBuilderCore.ThrowAsync(exc, targetContext: null); + AsyncStateMachineHelpers.ThrowAsync(exc, targetContext: null); } } /// Lazily instantiate the Task in a non-thread-safe manner. - private Task Task => m_task ?? (m_task = new Task()); + private Task Task => _builder.Task; /// /// Gets an object that may be used to uniquely identify this builder to the debugger. @@ -249,7 +165,7 @@ private void NotifySynchronizationContextOfCompletion() /// This property lazily instantiates the ID in a non-thread-safe manner. /// It must only be used by the debugger and AsyncCausalityTracer in a single-threaded manner. /// - private object ObjectIdForDebugger { get { return this.Task; } } + private object ObjectIdForDebugger => _builder.Task; } /// @@ -267,54 +183,25 @@ public struct AsyncTaskMethodBuilder private readonly static Task s_cachedCompleted = AsyncTaskMethodBuilder.s_defaultResultTask; /// The generic builder object to which this non-generic instance delegates. - private AsyncTaskMethodBuilder m_builder; // mutable struct: must not be readonly + private AsyncTaskMethodBuilder _builder; // mutable struct: must not be readonly /// Initializes a new . /// The initialized . - public static AsyncTaskMethodBuilder Create() - { - return default(AsyncTaskMethodBuilder); - // Note: If ATMB.Create is modified to do any initialization, this - // method needs to be updated to do m_builder = ATMB.Create(). - } + public static AsyncTaskMethodBuilder Create() => default(AsyncTaskMethodBuilder); /// Initiates the builder's execution with the associated state machine. /// Specifies the type of the state machine. /// The state machine instance, passed by reference. [DebuggerStepThrough] - public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine - { - // See comment on AsyncMethodBuilderCore.Start - // AsyncMethodBuilderCore.Start(ref stateMachine); - - if (stateMachine == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine); - Contract.EndContractBlock(); - - // Run the MoveNext method within a copy-on-write ExecutionContext scope. - // This allows us to undo any ExecutionContext changes made in MoveNext, - // so that they won't "leak" out of the first await. - - Thread currentThread = Thread.CurrentThread; - ExecutionContextSwitcher ecs = default(ExecutionContextSwitcher); - try - { - ExecutionContext.EstablishCopyOnWriteScope(currentThread, ref ecs); - stateMachine.MoveNext(); - } - finally - { - ecs.Undo(currentThread); - } - } + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => + _builder.Start(ref stateMachine); /// Associates the builder with the state machine it represents. /// The heap-allocated state machine object. /// The argument was null (Nothing in Visual Basic). /// The builder is incorrectly initialized. - public void SetStateMachine(IAsyncStateMachine stateMachine) - { - m_builder.SetStateMachine(stateMachine); // argument validation handled by AsyncMethodBuilderCore - } + public void SetStateMachine(IAsyncStateMachine stateMachine) => + _builder.SetStateMachine(stateMachine); /// /// Schedules the specified state machine to be pushed forward when the specified awaiter completes. @@ -326,10 +213,8 @@ public void SetStateMachine(IAsyncStateMachine stateMachine) public void AwaitOnCompleted( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion - where TStateMachine : IAsyncStateMachine - { - m_builder.AwaitOnCompleted(ref awaiter, ref stateMachine); - } + where TStateMachine : IAsyncStateMachine => + _builder.AwaitOnCompleted(ref awaiter, ref stateMachine); /// /// Schedules the specified state machine to be pushed forward when the specified awaiter completes. @@ -341,10 +226,8 @@ public void AwaitOnCompleted( public void AwaitUnsafeOnCompleted( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine - { - m_builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); - } + where TStateMachine : IAsyncStateMachine => + _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); /// Gets the for this builder. /// The representing the builder's asynchronous operation. @@ -352,7 +235,7 @@ public void AwaitUnsafeOnCompleted( public Task Task { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get { return m_builder.Task; } + get => _builder.Task; } /// @@ -361,12 +244,7 @@ public Task Task /// /// The builder is not initialized. /// The task has already completed. - public void SetResult() - { - // Accessing AsyncTaskMethodBuilder.s_cachedCompleted is faster than - // accessing AsyncTaskMethodBuilder.s_defaultResultTask. - m_builder.SetResult(s_cachedCompleted); - } + public void SetResult() => _builder.SetResult(s_cachedCompleted); // Using s_cachedCompleted is faster than using s_defaultResultTask. /// /// Completes the in the @@ -376,7 +254,7 @@ public void SetResult() /// The argument is null (Nothing in Visual Basic). /// The builder is not initialized. /// The task has already completed. - public void SetException(Exception exception) { m_builder.SetException(exception); } + public void SetException(Exception exception) => _builder.SetException(exception); /// /// Called by the debugger to request notification when the first wait operation @@ -385,10 +263,7 @@ public void SetResult() /// /// true to enable notification; false to disable a previously set notification. /// - internal void SetNotificationForWaitCompletion(bool enabled) - { - m_builder.SetNotificationForWaitCompletion(enabled); - } + internal void SetNotificationForWaitCompletion(bool enabled) => _builder.SetNotificationForWaitCompletion(enabled); /// /// Gets an object that may be used to uniquely identify this builder to the debugger. @@ -398,7 +273,7 @@ internal void SetNotificationForWaitCompletion(bool enabled) /// It must only be used by the debugger and tracing pruposes, and only in a single-threaded manner /// when no other threads are in the middle of accessing this property or this.Task. /// - private object ObjectIdForDebugger { get { return this.Task; } } + private object ObjectIdForDebugger => this.Task; } /// @@ -415,25 +290,12 @@ public struct AsyncTaskMethodBuilder /// A cached task for default(TResult). internal readonly static Task s_defaultResultTask = AsyncTaskCache.CreateCacheableTask(default(TResult)); - // WARNING: For performance reasons, the m_task field is lazily initialized. - // For correct results, the struct AsyncTaskMethodBuilder must - // always be used from the same location/copy, at least until m_task is - // initialized. If that guarantee is broken, the field could end up being - // initialized on the wrong copy. - - /// State related to the IAsyncStateMachine. - private AsyncMethodBuilderCore m_coreState; // mutable struct: must not be readonly /// The lazily-initialized built task. - private Task m_task; // lazily-initialized: must not be readonly + private Task _taskAndStateMachine; // lazily-initialized: must not be readonly /// Initializes a new . /// The initialized . - public static AsyncTaskMethodBuilder Create() - { - return default(AsyncTaskMethodBuilder); - // NOTE: If this method is ever updated to perform more initialization, - // ATMB.Create must also be updated to call this Create method. - } + public static AsyncTaskMethodBuilder Create() => default(AsyncTaskMethodBuilder); /// Initiates the builder's execution with the associated state machine. /// Specifies the type of the state machine. @@ -441,11 +303,10 @@ public static AsyncTaskMethodBuilder Create() [DebuggerStepThrough] public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { - // See comment on AsyncMethodBuilderCore.Start - // AsyncMethodBuilderCore.Start(ref stateMachine); - - if (stateMachine == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine); - Contract.EndContractBlock(); + if (stateMachine == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine); + } // Run the MoveNext method within a copy-on-write ExecutionContext scope. // This allows us to undo any ExecutionContext changes made in MoveNext, @@ -470,7 +331,9 @@ public void Start(ref TStateMachine stateMachine) where TStateMac /// The builder is incorrectly initialized. public void SetStateMachine(IAsyncStateMachine stateMachine) { - m_coreState.SetStateMachine(stateMachine); // argument validation handled by AsyncMethodBuilderCore + if (stateMachine == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine); + if (_taskAndStateMachine != null) ThrowHelper.ThrowInvalidOperationException(ExceptionResource.AsyncMethodBuilder_InstanceNotInitialized); + _taskAndStateMachine = stateMachine as Task; } /// @@ -487,29 +350,11 @@ public void AwaitOnCompleted( { try { - AsyncMethodBuilderCore.MoveNextRunner runnerToInitialize = null; - var continuation = m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn ? this.Task : null, ref runnerToInitialize); - Debug.Assert(continuation != null, "GetCompletionAction should always return a valid action."); - - // If this is our first await, such that we've not yet boxed the state machine, do so now. - if (m_coreState.m_stateMachine == null) - { - // Force the Task to be initialized prior to the first suspending await so - // that the original stack-based builder has a reference to the right Task. - Task builtTask = this.Task; - - // Box the state machine, then tell the boxed instance to call back into its own builder, - // so we can cache the boxed reference. NOTE: The language compiler may choose to use - // a class instead of a struct for the state machine for debugging purposes; in such cases, - // the stateMachine will already be an object. - m_coreState.PostBoxInitialization(stateMachine, runnerToInitialize, builtTask); - } - - awaiter.OnCompleted(continuation); + awaiter.OnCompleted(GetStateMachineBox(ref stateMachine).Delegate); } catch (Exception e) { - AsyncMethodBuilderCore.ThrowAsync(e, targetContext: null); + AsyncStateMachineHelpers.ThrowAsync(e, targetContext: null); } } @@ -527,30 +372,56 @@ public void AwaitUnsafeOnCompleted( { try { - AsyncMethodBuilderCore.MoveNextRunner runnerToInitialize = null; - var continuation = m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn ? this.Task : null, ref runnerToInitialize); - Debug.Assert(continuation != null, "GetCompletionAction should always return a valid action."); + awaiter.UnsafeOnCompleted(GetStateMachineBox(ref stateMachine).Delegate); + } + catch (Exception e) + { + AsyncStateMachineHelpers.ThrowAsync(e, targetContext: null); + } + } - // If this is our first await, such that we've not yet boxed the state machine, do so now. - if (m_coreState.m_stateMachine == null) + /// Gets the "boxed" state machine object. + /// Specifies the type of the async state machine. + /// The state machine. + /// The "boxed" state machine. + private AsyncStateMachineBox GetStateMachineBox( + ref TStateMachine stateMachine) + where TStateMachine : IAsyncStateMachine + { + ExecutionContext currentContext = ExecutionContext.Capture(); + + // If we don't have a reference to a state machine already, this is the first time + // we're awaiting something, and we need to create a new state machine object and associated + // delegate. If we already have one, we just use and update the existing objects. + var box = _taskAndStateMachine as AsyncStateMachineBox; + if (box == null) + { + // Alert a listening debugger that we can't make forward progress unless it slips threads. + // If we don't do this, and a method that uses "await foo;" is invoked through funceval, + // we could end up hooking up a callback to push forward the async method's state machine, + // the debugger would then abort the funceval after it takes too long, and then continuing + // execution could result in another callback being hooked up. At that point we have + // multiple callbacks registered to push the state machine, which could result in bad behavior. + Debugger.NotifyOfCrossThreadDependency(); + + // Create the box and store the state machine into it. + _taskAndStateMachine = box = new AsyncStateMachineBox(); + box.StateMachine = stateMachine; + box.Context = currentContext; + box.Delegate = box.MoveNext; + if (AsyncCausalityTracer.LoggingOn) { - // Force the Task to be initialized prior to the first suspending await so - // that the original stack-based builder has a reference to the right Task. - Task builtTask = this.Task; - - // Box the state machine, then tell the boxed instance to call back into its own builder, - // so we can cache the boxed reference. NOTE: The language compiler may choose to use - // a class instead of a struct for the state machine for debugging purposes; in such cases, - // the stateMachine will already be an object. - m_coreState.PostBoxInitialization(stateMachine, runnerToInitialize, builtTask); + box.Delegate = AsyncStateMachineHelpers.OutputAsyncCausalityEvents(box, box.Delegate); } - - awaiter.UnsafeOnCompleted(continuation); } - catch (Exception e) + else if (box.Context != currentContext) { - AsyncMethodBuilderCore.ThrowAsync(e, targetContext: null); + // This is not the first await. If the context has changed since we last saw it, + // update it. + box.Context = currentContext; } + + return box; } /// Gets the for this builder. @@ -558,12 +429,12 @@ public void AwaitUnsafeOnCompleted( public Task Task { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get { return m_task ?? InitializeTask(); } + get => _taskAndStateMachine ?? InitializeTask(); } /// Initializes the task, which must not yet be initialized. [MethodImpl(MethodImplOptions.NoInlining)] - private Task InitializeTask() => (m_task = new Task()); + private Task InitializeTask() => (_taskAndStateMachine = new Task()); /// /// Completes the in the @@ -575,10 +446,10 @@ public void SetResult(TResult result) { // Get the currently stored task, which will be non-null if get_Task has already been accessed. // If there isn't one, get a task and store it. - if (m_task == null) + if (_taskAndStateMachine == null) { - m_task = GetTaskForResult(result); - Debug.Assert(m_task != null, "GetTaskForResult should never return null"); + _taskAndStateMachine = GetTaskForResult(result); + Debug.Assert(_taskAndStateMachine != null, $"{nameof(GetTaskForResult)} should never return null"); } else { @@ -591,14 +462,14 @@ public void SetResult(TResult result) /// The result to use to complete the task. private void SetExistingTaskResult(TResult result) { - Debug.Assert(m_task != null, "Expected non-null task"); + Debug.Assert(_taskAndStateMachine != null, "Expected non-null task"); if (AsyncCausalityTracer.LoggingOn || System.Threading.Tasks.Task.s_asyncDebuggingEnabled) { LogExistingTaskCompletion(); } - if (!m_task.TrySetResult(result)) + if (!_taskAndStateMachine.TrySetResult(result)) { ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted); } @@ -607,17 +478,17 @@ private void SetExistingTaskResult(TResult result) /// Handles logging for the successful completion of an operation. private void LogExistingTaskCompletion() { - Debug.Assert(m_task != null); + Debug.Assert(_taskAndStateMachine != null); if (AsyncCausalityTracer.LoggingOn) { - AsyncCausalityTracer.TraceOperationCompletion(CausalityTraceLevel.Required, m_task.Id, AsyncCausalityStatus.Completed); + AsyncCausalityTracer.TraceOperationCompletion(CausalityTraceLevel.Required, _taskAndStateMachine.Id, AsyncCausalityStatus.Completed); } // only log if we have a real task that was previously created if (System.Threading.Tasks.Task.s_asyncDebuggingEnabled) { - System.Threading.Tasks.Task.RemoveFromActiveTasks(m_task.Id); + System.Threading.Tasks.Task.RemoveFromActiveTasks(_taskAndStateMachine.Id); } } @@ -630,13 +501,13 @@ private void LogExistingTaskCompletion() internal void SetResult(Task completedTask) { Contract.Requires(completedTask != null, "Expected non-null task"); - Contract.Requires(completedTask.Status == TaskStatus.RanToCompletion, "Expected a successfully completed task"); + Contract.Requires(completedTask.IsCompletedSuccessfully, "Expected a successfully completed task"); // Get the currently stored task, which will be non-null if get_Task has already been accessed. // If there isn't one, store the supplied completed task. - if (m_task == null) + if (_taskAndStateMachine == null) { - m_task = completedTask; + _taskAndStateMachine = completedTask; } else { @@ -654,11 +525,12 @@ internal void SetResult(Task completedTask) /// The task has already completed. public void SetException(Exception exception) { - if (exception == null) throw new ArgumentNullException(nameof(exception)); - Contract.EndContractBlock(); - + if (exception == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.exception); + } - var task = m_task; + Task task = _taskAndStateMachine; if (task == null) { // Get the task, forcing initialization if it hasn't already been initialized. @@ -671,7 +543,7 @@ public void SetException(Exception exception) task.TrySetCanceled(oce.CancellationToken, oce) : task.TrySetException(exception); - // Unlike with TaskCompletionSource, we do not need to spin here until m_task is completed, + // Unlike with TaskCompletionSource, we do not need to spin here until _taskAndStateMachine is completed, // since AsyncTaskMethodBuilder.SetException should not be immediately followed by any code // that depends on the task having completely completed. Moreover, with correct usage, // SetResult or SetException should only be called once, so the Try* methods should always @@ -709,7 +581,7 @@ internal void SetNotificationForWaitCompletion(bool enabled) /// It must only be used by the debugger and tracing purposes, and only in a single-threaded manner /// when no other threads are in the middle of accessing this property or this.Task. /// - private object ObjectIdForDebugger { get { return this.Task; } } + private object ObjectIdForDebugger => this.Task; /// /// Gets a task for the specified result. This will either @@ -836,112 +708,62 @@ private static Task[] CreateInt32Tasks() /// Specifies the result type. /// The result for the task. /// The cacheable task. - internal static Task CreateCacheableTask(TResult result) - { - return new Task(false, result, (TaskCreationOptions)InternalTaskOptions.DoNotDispose, default(CancellationToken)); - } + internal static Task CreateCacheableTask(TResult result) => + new Task(false, result, (TaskCreationOptions)InternalTaskOptions.DoNotDispose, default(CancellationToken)); } - /// Holds state related to the builder's IAsyncStateMachine. - /// This is a mutable struct. Be very delicate with it. - internal struct AsyncMethodBuilderCore + /// 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. + internal sealed class AsyncStateMachineBox : + Task, IAsyncStateMachine, IDebuggingAsyncStateMachineAccessor + where TStateMachine : IAsyncStateMachine { - /// A reference to the heap-allocated state machine object associated with this builder. - internal IAsyncStateMachine m_stateMachine; - /// A cached Action delegate used when dealing with a default ExecutionContext. - internal Action m_defaultContextAction; + /// 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(); - // This method is copy&pasted into the public Start methods to avoid size overhead of valuetype generic instantiations. - // Ideally, we would build intrinsics to get the raw ref address and raw code address of MoveNext, and just use the shared implementation. + /// 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; + /// A delegate to the method. + public Action Delegate; - /// Associates the builder with the state machine it represents. - /// The heap-allocated state machine object. - /// The argument was null (Nothing in Visual Basic). - /// The builder is incorrectly initialized. - public void SetStateMachine(IAsyncStateMachine stateMachine) + /// Call MoveNext on . + public void MoveNext() { - if (stateMachine == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine); - Contract.EndContractBlock(); - if (m_stateMachine != null) ThrowHelper.ThrowInvalidOperationException(ExceptionResource.AsyncMethodBuilder_InstanceNotInitialized); - m_stateMachine = stateMachine; - } - - /// - /// Gets the Action to use with an awaiter's OnCompleted or UnsafeOnCompleted method. - /// On first invocation, the supplied state machine will be boxed. - /// - /// Specifies the type of the method builder used. - /// Specifies the type of the state machine used. - /// The builder. - /// The state machine. - /// An Action to provide to the awaiter. - internal Action GetCompletionAction(Task taskForTracing, ref MoveNextRunner runnerToInitialize) - { - Debug.Assert(m_defaultContextAction == null || m_stateMachine != null, - "Expected non-null m_stateMachine on non-null m_defaultContextAction"); - - // Alert a listening debugger that we can't make forward progress unless it slips threads. - // If we don't do this, and a method that uses "await foo;" is invoked through funceval, - // we could end up hooking up a callback to push forward the async method's state machine, - // the debugger would then abort the funceval after it takes too long, and then continuing - // execution could result in another callback being hooked up. At that point we have - // multiple callbacks registered to push the state machine, which could result in bad behavior. - Debugger.NotifyOfCrossThreadDependency(); - - // The builder needs to flow ExecutionContext, so capture it. - var capturedContext = ExecutionContext.Capture(); - - // If the ExecutionContext is the default context, try to use a cached delegate, creating one if necessary. - Action action; - MoveNextRunner runner; - if (capturedContext == ExecutionContext.Default) + if (Context == null) { - // Get the cached delegate, and if it's non-null, return it. - action = m_defaultContextAction; - if (action != null) - { - Debug.Assert(m_stateMachine != null, "If the delegate was set, the state machine should have been as well."); - return action; - } - - // There wasn't a cached delegate, so create one and cache it. - // The delegate won't be usable until we set the MoveNextRunner's target state machine. - runner = new MoveNextRunner(m_stateMachine); - - action = new Action(runner.RunWithDefaultContext); - if (taskForTracing != null) - { - action = OutputAsyncCausalityEvents(taskForTracing, action); - } - m_defaultContextAction = action; + StateMachine.MoveNext(); } - // Otherwise, create an Action that flows this context. The context may be null. - // The delegate won't be usable until we set the MoveNextRunner's target state machine. else { - var runnerWithContext = new MoveNextRunnerWithContext(capturedContext, m_stateMachine); - runner = runnerWithContext; - action = new Action(runnerWithContext.RunWithCapturedContext); - - if (taskForTracing != null) - { - action = OutputAsyncCausalityEvents(taskForTracing, action); - } - - // NOTE: If capturedContext is null, we could create the Action to point directly - // to m_stateMachine.MoveNext. However, that follows a much more expensive - // delegate creation path. + ExecutionContext.Run(Context, s_callback, this); } + } - if (m_stateMachine == null) - runnerToInitialize = runner; + /// Store the supplied state machine. + public void SetStateMachine(IAsyncStateMachine stateMachine) => StateMachine.SetStateMachine(stateMachine); - return action; - } + /// Gets the state machine as a boxed object. This should only be used for debugging purposes. + IAsyncStateMachine IDebuggingAsyncStateMachineAccessor.GetStateMachineObject() => StateMachine; // boxes, only use for debugging + } - private Action OutputAsyncCausalityEvents(Task innerTask, Action continuation) - { - return CreateContinuationWrapper(continuation, () => + /// + /// An interface implemented by to allow access + /// to its StateMachine without knowing the generic types associated with a given instance. + /// + interface IDebuggingAsyncStateMachineAccessor + { + /// Gets the state machine as a boxed object. This should only be used for debugging purposes. + IAsyncStateMachine GetStateMachineObject(); + } + + /// Shared helpers for manipulating state related to async state machines. + internal static class AsyncStateMachineHelpers + { + internal static Action OutputAsyncCausalityEvents(Task innerTask, Action continuation) => + CreateContinuationWrapper(continuation, () => { AsyncCausalityTracer.TraceSynchronousWorkStart(CausalityTraceLevel.Required, innerTask.Id, CausalitySynchronousWork.Execution); @@ -950,30 +772,64 @@ private Action OutputAsyncCausalityEvents(Task innerTask, Action continuation) AsyncCausalityTracer.TraceSynchronousWorkCompletion(CausalityTraceLevel.Required, CausalitySynchronousWork.Execution); }, innerTask); - } - internal void PostBoxInitialization(IAsyncStateMachine stateMachine, MoveNextRunner runner, Task builtTask) + internal static Action CreateContinuationWrapper(Action continuation, Action invokeAction, Task innerTask = null) => + new ContinuationWrapper(continuation, invokeAction, innerTask).Invoke; + + internal static Action TryGetStateMachineForDebugger(Action action) { - if (builtTask != null) + object target = action.Target; + + var sm = target as IDebuggingAsyncStateMachineAccessor; + if (sm != null) { - if (AsyncCausalityTracer.LoggingOn) - AsyncCausalityTracer.TraceOperationCreation(CausalityTraceLevel.Required, builtTask.Id, "Async: " + stateMachine.GetType().Name, 0); + return sm.GetStateMachineObject().MoveNext; + } - if (System.Threading.Tasks.Task.s_asyncDebuggingEnabled) - System.Threading.Tasks.Task.AddToActiveTasks(builtTask); + var continuationWrapper = target as ContinuationWrapper; + if (continuationWrapper != null) + { + return TryGetStateMachineForDebugger(continuationWrapper._continuation); } - m_stateMachine = stateMachine; - m_stateMachine.SetStateMachine(m_stateMachine); + return action; + } - Debug.Assert(runner.m_stateMachine == null, "The runner's state machine should not yet have been populated."); - Debug.Assert(m_stateMachine != null, "The builder's state machine field should have been initialized."); + /// + /// Logically we pass just an Action (delegate) to a task for its action to 'ContinueWith' when it completes. + /// However debuggers and profilers need more information about what that action is. (In particular what + /// the action after that is and after that. To solve this problem we create a 'ContinuationWrapper + /// which when invoked just does the original action (the invoke action), but also remembers other information + /// (like the action after that (which is also a ContinuationWrapper and thus form a linked list). + // We also store that task if the action is associate with at task. + /// + private class ContinuationWrapper + { + internal readonly Action _continuation; // This is continuation which will happen after m_invokeAction (and is probably a ContinuationWrapper) + private readonly Action _invokeAction; // This wrapper is an action that wraps another action, this is that Action. + internal readonly Task _innerTask; // If the continuation is logically going to invoke a task, this is that task (may be null) - // Now that we have the state machine, store it into the runner that the action delegate points to. - // And return the action. - runner.m_stateMachine = m_stateMachine; // only after this line is the Action delegate usable + internal ContinuationWrapper(Action continuation, Action invokeAction, Task innerTask) + { + Contract.Requires(continuation != null, "Expected non-null continuation"); + + // If we don't have a task, see if our continuation is a wrapper and use that. + if (innerTask == null) + { + innerTask = GetContinuationTask(continuation); + } + + _continuation = continuation; + _innerTask = innerTask; + _invokeAction = invokeAction; + } + + internal void Invoke() => _invokeAction(); } + internal static Task GetContinuationTask(Action continuation) => + (continuation?.Target as ContinuationWrapper)?._innerTask; + /// Throws the exception on the ThreadPool. /// The exception to propagate. /// The target context on which to propagate the exception. Null to use the ThreadPool. @@ -1008,128 +864,5 @@ internal static void ThrowAsync(Exception exception, SynchronizationContext targ ThreadPool.QueueUserWorkItem(state => ((ExceptionDispatchInfo)state).Throw(), edi); } } - - /// Provides the ability to invoke a state machine's MoveNext method under a supplied ExecutionContext. - internal sealed class MoveNextRunnerWithContext : MoveNextRunner - { - /// The context with which to run MoveNext. - private readonly ExecutionContext m_context; - - /// Initializes the runner. - /// The context with which to run MoveNext. - internal MoveNextRunnerWithContext(ExecutionContext context, IAsyncStateMachine stateMachine) : base(stateMachine) - { - m_context = context; - } - - /// Invokes MoveNext under the provided context. - internal void RunWithCapturedContext() - { - Debug.Assert(m_stateMachine != null, "The state machine must have been set before calling Run."); - - if (m_context != null) - { - // Use the context and callback to invoke m_stateMachine.MoveNext. - ExecutionContext.Run(m_context, InvokeMoveNextCallback, m_stateMachine); - } - else - { - m_stateMachine.MoveNext(); - } - } - } - - /// Provides the ability to invoke a state machine's MoveNext method. - internal class MoveNextRunner - { - /// The state machine whose MoveNext method should be invoked. - internal IAsyncStateMachine m_stateMachine; - - /// Initializes the runner. - internal MoveNextRunner(IAsyncStateMachine stateMachine) - { - m_stateMachine = stateMachine; - } - - /// Invokes MoveNext under the default context. - internal void RunWithDefaultContext() - { - Debug.Assert(m_stateMachine != null, "The state machine must have been set before calling Run."); - ExecutionContext.Run(ExecutionContext.Default, InvokeMoveNextCallback, m_stateMachine); - } - - /// Gets a delegate to the InvokeMoveNext method. - protected static readonly ContextCallback InvokeMoveNextCallback = sm => ((IAsyncStateMachine)sm).MoveNext(); - } - - /// - /// Logically we pass just an Action (delegate) to a task for its action to 'ContinueWith' when it completes. - /// However debuggers and profilers need more information about what that action is. (In particular what - /// the action after that is and after that. To solve this problem we create a 'ContinuationWrapper - /// which when invoked just does the original action (the invoke action), but also remembers other information - /// (like the action after that (which is also a ContinuationWrapper and thus form a linked list). - // We also store that task if the action is associate with at task. - /// - private class ContinuationWrapper - { - internal readonly Action m_continuation; // This is continuation which will happen after m_invokeAction (and is probably a ContinuationWrapper) - private readonly Action m_invokeAction; // This wrapper is an action that wraps another action, this is that Action. - internal readonly Task m_innerTask; // If the continuation is logically going to invoke a task, this is that task (may be null) - - internal ContinuationWrapper(Action continuation, Action invokeAction, Task innerTask) - { - Contract.Requires(continuation != null, "Expected non-null continuation"); - - // If we don't have a task, see if our continuation is a wrapper and use that. - if (innerTask == null) - innerTask = TryGetContinuationTask(continuation); - - m_continuation = continuation; - m_innerTask = innerTask; - m_invokeAction = invokeAction; - } - - internal void Invoke() - { - m_invokeAction(); - } - } - - internal static Action CreateContinuationWrapper(Action continuation, Action invokeAction, Task innerTask = null) - { - return new ContinuationWrapper(continuation, invokeAction, innerTask).Invoke; - } - - internal static Action TryGetStateMachineForDebugger(Action action) - { - object target = action.Target; - var runner = target as AsyncMethodBuilderCore.MoveNextRunner; - if (runner != null) - { - return new Action(runner.m_stateMachine.MoveNext); - } - - var continuationWrapper = target as ContinuationWrapper; - if (continuationWrapper != null) - { - return TryGetStateMachineForDebugger(continuationWrapper.m_continuation); - } - - return action; - } - - /// - /// Given an action, see if it is a contiunation wrapper and has a Task associated with it. If so return it (null otherwise) - /// - internal static Task TryGetContinuationTask(Action action) - { - if (action != null) - { - var asWrapper = action.Target as ContinuationWrapper; - if (asWrapper != null) - return asWrapper.m_innerTask; - } - return null; - } } } diff --git a/src/mscorlib/src/System/Runtime/CompilerServices/TaskAwaiter.cs b/src/mscorlib/src/System/Runtime/CompilerServices/TaskAwaiter.cs index c35658e54cdc..271d9343aa6f 100644 --- a/src/mscorlib/src/System/Runtime/CompilerServices/TaskAwaiter.cs +++ b/src/mscorlib/src/System/Runtime/CompilerServices/TaskAwaiter.cs @@ -236,7 +236,7 @@ private static Action OutputWaitEtwEvents(Task task, Action continuation) var currentTaskAtBegin = Task.InternalCurrent; // If this task's continuation is another task, get it. - var continuationTask = AsyncMethodBuilderCore.TryGetContinuationTask(continuation); + Task continuationTask = AsyncStateMachineHelpers.GetContinuationTask(continuation); etwLog.TaskWaitBegin( (currentTaskAtBegin != null ? currentTaskAtBegin.m_taskScheduler.Id : TaskScheduler.Default.Id), (currentTaskAtBegin != null ? currentTaskAtBegin.Id : 0), @@ -249,7 +249,7 @@ private static Action OutputWaitEtwEvents(Task task, Action continuation) // is enabled, and in doing so it allows us to pass the awaited task's information into the end event // in a purely pay-for-play manner (the alternatively would be to increase the size of TaskAwaiter // just for this ETW purpose, not pay-for-play, since GetResult would need to know whether a real yield occurred). - return AsyncMethodBuilderCore.CreateContinuationWrapper(continuation, () => + return AsyncStateMachineHelpers.CreateContinuationWrapper(continuation, () => { if (Task.s_asyncDebuggingEnabled) { diff --git a/src/mscorlib/src/System/Runtime/CompilerServices/YieldAwaitable.cs b/src/mscorlib/src/System/Runtime/CompilerServices/YieldAwaitable.cs index f1c777252632..d1a664e02232 100644 --- a/src/mscorlib/src/System/Runtime/CompilerServices/YieldAwaitable.cs +++ b/src/mscorlib/src/System/Runtime/CompilerServices/YieldAwaitable.cs @@ -124,7 +124,7 @@ private static Action OutputCorrelationEtwEvent(Action continuation) // fire the correlation ETW event TplEtwProvider.Log.AwaitTaskContinuationScheduled(TaskScheduler.Current.Id, (currentTask != null) ? currentTask.Id : 0, continuationId); - return AsyncMethodBuilderCore.CreateContinuationWrapper(continuation, () => + return AsyncStateMachineHelpers.CreateContinuationWrapper(continuation, () => { var etwLog = TplEtwProvider.Log; etwLog.TaskWaitContinuationStarted(continuationId); diff --git a/src/mscorlib/src/System/Threading/Tasks/Task.cs b/src/mscorlib/src/System/Threading/Tasks/Task.cs index c19fd3ec64b5..e435077ee6bc 100644 --- a/src/mscorlib/src/System/Threading/Tasks/Task.cs +++ b/src/mscorlib/src/System/Threading/Tasks/Task.cs @@ -6130,7 +6130,7 @@ internal static Delegate[] GetDelegatesFromContinuationObject(object continuatio Action singleAction = continuationObject as Action; if (singleAction != null) { - return new Delegate[] { AsyncMethodBuilderCore.TryGetStateMachineForDebugger(singleAction) }; + return new Delegate[] { AsyncStateMachineHelpers.TryGetStateMachineForDebugger(singleAction) }; } TaskContinuation taskContinuation = continuationObject as TaskContinuation; diff --git a/src/mscorlib/src/System/Threading/Tasks/TaskContinuation.cs b/src/mscorlib/src/System/Threading/Tasks/TaskContinuation.cs index de222352c9d9..8ec7d663e4aa 100644 --- a/src/mscorlib/src/System/Threading/Tasks/TaskContinuation.cs +++ b/src/mscorlib/src/System/Threading/Tasks/TaskContinuation.cs @@ -812,7 +812,7 @@ protected static void ThrowAsyncIfNecessary(Exception exc) internal override Delegate[] GetDelegateContinuationsForDebugger() { Debug.Assert(m_action != null); - return new Delegate[] { AsyncMethodBuilderCore.TryGetStateMachineForDebugger(m_action) }; + return new Delegate[] { AsyncStateMachineHelpers.TryGetStateMachineForDebugger(m_action) }; } } }