diff --git a/src/System.Private.CoreLib/shared/System/Runtime/CompilerServices/AsyncIteratorMethodBuilder.cs b/src/System.Private.CoreLib/shared/System/Runtime/CompilerServices/AsyncIteratorMethodBuilder.cs index 6fc5706897fe..fee9d40cdfcf 100644 --- a/src/System.Private.CoreLib/shared/System/Runtime/CompilerServices/AsyncIteratorMethodBuilder.cs +++ b/src/System.Private.CoreLib/shared/System/Runtime/CompilerServices/AsyncIteratorMethodBuilder.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using System.Threading; +using System.Threading.Tasks; namespace System.Runtime.CompilerServices { @@ -12,29 +13,29 @@ namespace System.Runtime.CompilerServices public struct AsyncIteratorMethodBuilder { // AsyncIteratorMethodBuilder is used by the language compiler as part of generating - // async iterators. For now, the implementation just wraps AsyncTaskMethodBuilder, as - // most of the logic is shared. However, in the future this could be changed and + // async iterators. For now, the implementation just forwards to AsyncMethodBuilderCore, + // as most of the logic is shared. However, in the future this could be changed and // optimized. For example, we do need to allocate an object (once) to flow state like - // ExecutionContext, which AsyncTaskMethodBuilder handles, but it handles it by + // ExecutionContext, which AsyncMethodBuilderCore handles, but it handles it by // allocating a Task-derived object. We could optimize this further by removing // the Task from the hierarchy, but in doing so we'd also lose a variety of optimizations // related to it, so we'd need to replicate all of those optimizations (e.g. storing // that box object directly into a Task's continuation field). - private AsyncTaskMethodBuilder _methodBuilder; // mutable struct; do not make it readonly + /// The lazily-initialized built task. + private Task m_task; // Debugger depends on the exact name of this field. /// Creates an instance of the struct. /// The initialized instance. - public static AsyncIteratorMethodBuilder Create() => + public static AsyncIteratorMethodBuilder Create() + { #if PROJECTN - // ProjectN's AsyncTaskMethodBuilder.Create() currently does additional debugger-related - // work, so we need to delegate to it. - new AsyncIteratorMethodBuilder() { _methodBuilder = AsyncTaskMethodBuilder.Create() }; + var result = new AsyncIteratorMethodBuilder(); + return AsyncMethodBuilderCore.InitalizeTaskIfDebugging(ref result, ref result.m_task!); // TODO-NULLABLE: Remove ! when nullable attributes are respected #else - // _methodBuilder should be initialized to AsyncTaskMethodBuilder.Create(), but on coreclr - // that Create() is a nop, so we can just return the default here. - default; + return default; #endif + } /// Invokes on the state machine while guarding the . /// The type of the state machine. @@ -50,8 +51,8 @@ public struct AsyncIteratorMethodBuilder /// The state machine. public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion - where TStateMachine : IAsyncStateMachine => - _methodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine); + where TStateMachine : IAsyncStateMachine + => AsyncMethodBuilderCore.AwaitOnCompleted(ref awaiter, ref stateMachine, ref m_task); /// Schedules the state machine to proceed to the next action when the specified awaiter completes. /// The type of the awaiter. @@ -60,13 +61,13 @@ public struct AsyncIteratorMethodBuilder /// The state machine. public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine => - _methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + where TStateMachine : IAsyncStateMachine + => AsyncMethodBuilderCore.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref m_task); /// Marks iteration as being completed, whether successfully or otherwise. - public void Complete() => _methodBuilder.SetResult(); + public void Complete() => AsyncMethodBuilderCore.SetResult(ref m_task, Task.s_cachedCompleted); /// Gets an object that may be used to uniquely identify this builder to the debugger. - internal object ObjectIdForDebugger => _methodBuilder.ObjectIdForDebugger; + internal object ObjectIdForDebugger => AsyncMethodBuilderCore.ObjectIdForDebugger(ref m_task); } } diff --git a/src/System.Private.CoreLib/shared/System/Runtime/CompilerServices/AsyncMethodBuilder.cs b/src/System.Private.CoreLib/shared/System/Runtime/CompilerServices/AsyncMethodBuilder.cs index d08753a088c6..d070a1d75dd1 100644 --- a/src/System.Private.CoreLib/shared/System/Runtime/CompilerServices/AsyncMethodBuilder.cs +++ b/src/System.Private.CoreLib/shared/System/Runtime/CompilerServices/AsyncMethodBuilder.cs @@ -13,7 +13,6 @@ using System.Diagnostics; using System.Diagnostics.Tracing; using System.Reflection; -using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; using System.Text; @@ -30,8 +29,8 @@ public struct AsyncVoidMethodBuilder { /// The synchronization context associated with this operation. private SynchronizationContext? _synchronizationContext; - /// The builder this void builder wraps. - private AsyncTaskMethodBuilder _builder; // mutable struct: must not be readonly + /// The lazily-initialized built task. + private Task m_task; // Debugger depends on the exact name of this field. /// Initializes a new . /// The initialized . @@ -39,14 +38,11 @@ public static AsyncVoidMethodBuilder Create() { SynchronizationContext? sc = SynchronizationContext.Current; sc?.OperationStarted(); + var result = new AsyncVoidMethodBuilder() { _synchronizationContext = sc }; #if PROJECTN - // ProjectN's AsyncTaskMethodBuilder.Create() currently does additional debugger-related - // work, so we need to delegate to it. - return new AsyncVoidMethodBuilder() { _synchronizationContext = sc, _builder = AsyncTaskMethodBuilder.Create() }; + return AsyncMethodBuilderCore.InitalizeTaskIfDebugging(ref result, ref result.m_task!); // TODO-NULLABLE: Remove ! when nullable attributes are respected #else - // _builder should be initialized to AsyncTaskMethodBuilder.Create(), but on coreclr - // that Create() is a nop, so we can just return the default here. - return new AsyncVoidMethodBuilder() { _synchronizationContext = sc }; + return result; #endif } @@ -63,8 +59,8 @@ public static AsyncVoidMethodBuilder Create() /// The heap-allocated state machine object. /// The argument was null (Nothing in Visual Basic). /// The builder is incorrectly initialized. - public void SetStateMachine(IAsyncStateMachine stateMachine) => - _builder.SetStateMachine(stateMachine); + public void SetStateMachine(IAsyncStateMachine stateMachine) + => AsyncMethodBuilderCore.SetStateMachine(stateMachine, m_task); /// /// Schedules the specified state machine to be pushed forward when the specified awaiter completes. @@ -76,8 +72,8 @@ public static AsyncVoidMethodBuilder Create() public void AwaitOnCompleted( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion - where TStateMachine : IAsyncStateMachine => - _builder.AwaitOnCompleted(ref awaiter, ref stateMachine); + where TStateMachine : IAsyncStateMachine + => AsyncMethodBuilderCore.AwaitOnCompleted(ref awaiter, ref stateMachine, ref m_task); /// /// Schedules the specified state machine to be pushed forward when the specified awaiter completes. @@ -89,8 +85,8 @@ public static AsyncVoidMethodBuilder Create() public void AwaitUnsafeOnCompleted( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine => - _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + where TStateMachine : IAsyncStateMachine + => AsyncMethodBuilderCore.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref m_task); /// Completes the method builder successfully. public void SetResult() @@ -102,7 +98,7 @@ public void SetResult() // Mark the builder as completed. As this is a void-returning method, this mostly // doesn't matter, but it can affect things like debug events related to finalization. - _builder.SetResult(); + AsyncMethodBuilderCore.SetResult(ref m_task, Task.s_cachedCompleted); if (_synchronizationContext != null) { @@ -148,7 +144,7 @@ public void SetException(Exception exception) } // The exception was propagated already; we don't need or want to fault the builder, just mark it as completed. - _builder.SetResult(); + AsyncMethodBuilderCore.SetResult(ref m_task, Task.s_cachedCompleted); } /// Notifies the current synchronization context that the operation completed. @@ -168,7 +164,11 @@ private void NotifySynchronizationContextOfCompletion() } /// Lazily instantiate the Task in a non-thread-safe manner. - private Task Task => _builder.Task; + public Task Task + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => m_task ?? AsyncMethodBuilderCore.InitializeTaskAsPromise(ref m_task!); // TODO-NULLABLE: Remove ! when nullable attributes are respected + } /// /// Gets an object that may be used to uniquely identify this builder to the debugger. @@ -177,7 +177,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. /// - internal object ObjectIdForDebugger => _builder.ObjectIdForDebugger; + internal object ObjectIdForDebugger => AsyncMethodBuilderCore.ObjectIdForDebugger(ref m_task); } /// @@ -191,28 +191,20 @@ private void NotifySynchronizationContextOfCompletion() /// public struct AsyncTaskMethodBuilder { - /// A cached VoidTaskResult task used for builders that complete synchronously. -#if PROJECTN - private static readonly Task s_cachedCompleted = AsyncTaskCache.CreateCacheableTask(default(VoidTaskResult)); -#else - private static readonly Task s_cachedCompleted = AsyncTaskMethodBuilder.s_defaultResultTask; -#endif - - /// The generic builder object to which this non-generic instance delegates. - private AsyncTaskMethodBuilder m_builder; // mutable struct: must not be readonly. Debugger depends on the exact name of this field. + /// The lazily-initialized built task. + private Task m_task; // Debugger depends on the exact name of this field. /// Initializes a new . /// The initialized . - public static AsyncTaskMethodBuilder Create() => + public static AsyncTaskMethodBuilder Create() + { #if PROJECTN - // ProjectN's AsyncTaskMethodBuilder.Create() currently does additional debugger-related - // work, so we need to delegate to it. - new AsyncTaskMethodBuilder { m_builder = AsyncTaskMethodBuilder.Create() }; + var result = new AsyncTaskMethodBuilder(); + return AsyncMethodBuilderCore.InitalizeTaskIfDebugging(ref result, ref result.m_task!); // TODO-NULLABLE: Remove ! when nullable attributes are respected #else - // m_builder should be initialized to AsyncTaskMethodBuilder.Create(), but on coreclr - // that Create() is a nop, so we can just return the default here. - default; + return default; #endif + } /// Initiates the builder's execution with the associated state machine. /// Specifies the type of the state machine. @@ -226,8 +218,8 @@ public struct AsyncTaskMethodBuilder /// 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); + public void SetStateMachine(IAsyncStateMachine stateMachine) + => AsyncMethodBuilderCore.SetStateMachine(stateMachine, m_task); /// /// Schedules the specified state machine to be pushed forward when the specified awaiter completes. @@ -239,8 +231,8 @@ public struct AsyncTaskMethodBuilder 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 + => AsyncMethodBuilderCore.AwaitOnCompleted(ref awaiter, ref stateMachine, ref m_task); /// /// Schedules the specified state machine to be pushed forward when the specified awaiter completes. @@ -252,8 +244,8 @@ public struct AsyncTaskMethodBuilder 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 + => AsyncMethodBuilderCore.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref m_task); /// Gets the for this builder. /// The representing the builder's asynchronous operation. @@ -261,7 +253,7 @@ public struct AsyncTaskMethodBuilder public Task Task { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => m_builder.Task; + get => m_task ?? AsyncMethodBuilderCore.InitializeTaskAsPromise(ref m_task!); // TODO-NULLABLE: Remove ! when nullable attributes are respected } /// @@ -270,7 +262,8 @@ public Task Task /// /// The builder is not initialized. /// The task has already completed. - public void SetResult() => m_builder.SetResult(s_cachedCompleted); // Using s_cachedCompleted is faster than using s_defaultResultTask. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetResult() => AsyncMethodBuilderCore.SetResult(ref m_task, Task.s_cachedCompleted); /// /// Completes the in the @@ -280,7 +273,7 @@ public Task Task /// 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) => AsyncMethodBuilderCore.SetException(ref m_task, exception); /// /// Called by the debugger to request notification when the first wait operation @@ -289,7 +282,8 @@ public Task Task /// /// 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) + => AsyncMethodBuilderCore.SetNotificationForWaitCompletion(ref m_task, enabled); /// /// Gets an object that may be used to uniquely identify this builder to the debugger. @@ -299,7 +293,7 @@ public Task Task /// 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. /// - internal object ObjectIdForDebugger => m_builder.ObjectIdForDebugger; + internal object ObjectIdForDebugger => AsyncMethodBuilderCore.ObjectIdForDebugger(ref m_task); } /// @@ -313,13 +307,8 @@ public Task Task /// public struct AsyncTaskMethodBuilder { -#if !PROJECTN - /// A cached task for default(TResult). - internal readonly static Task s_defaultResultTask = AsyncTaskCache.CreateCacheableTask(default(TResult)!); // TODO-NULLABLE: Remove ! when nullable attributes are respected -#endif - /// The lazily-initialized built task. - private Task m_task; // lazily-initialized: must not be readonly. Debugger depends on the exact name of this field. + private Task m_task; // Debugger depends on the exact name of this field. /// Initializes a new . /// The initialized . @@ -327,16 +316,11 @@ public static AsyncTaskMethodBuilder Create() { #if PROJECTN var result = new AsyncTaskMethodBuilder(); - if (System.Threading.Tasks.Task.s_asyncDebuggingEnabled) - { - // This allows the debugger to access m_task directly without evaluating ObjectIdForDebugger for ProjectN - result.InitializeTaskAsStateMachineBox(); - } - return result; + return AsyncMethodBuilderCore.InitalizeTaskIfDebugging(ref result, ref result.m_task!); // TODO-NULLABLE: Remove ! when nullable attributes are respected #else // NOTE: If this method is ever updated to perform more initialization, // other Create methods like AsyncTaskMethodBuilder.Create and - // AsyncValueTaskMethodBuilder.Create must be updated to call this. + // AsyncValueTaskMethodBuilder.Create must be updated also. return default; #endif } @@ -367,16 +351,7 @@ public void SetStateMachine(IAsyncStateMachine stateMachine) ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine - { - try - { - awaiter.OnCompleted(GetStateMachineBox(ref stateMachine).MoveNextAction); - } - catch (Exception e) - { - System.Threading.Tasks.Task.ThrowAsync(e, targetContext: null); - } - } + => AsyncMethodBuilderCore.AwaitOnCompleted(ref awaiter, ref stateMachine, ref m_task); /// /// Schedules the specified state machine to be pushed forward when the specified awaiter completes. @@ -385,151 +360,16 @@ public void SetStateMachine(IAsyncStateMachine stateMachine) /// Specifies the type of the state machine. /// The awaiter. /// The state machine. - // AggressiveOptimization to workaround boxing allocations in Tier0 until: https://github.com/dotnet/coreclr/issues/14474 - [MethodImpl(MethodImplOptions.AggressiveOptimization)] public void AwaitUnsafeOnCompleted( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine - { - IAsyncStateMachineBox box = GetStateMachineBox(ref stateMachine); - - // The null tests here ensure that the jit can optimize away the interface - // tests when TAwaiter is a ref type. - - if ((null != (object)default(TAwaiter)!) && (awaiter is ITaskAwaiter)) // TODO-NULLABLE: default(T) == null warning (https://github.com/dotnet/roslyn/issues/34757) - { - 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 ((null != (object)default(TAwaiter)!) && (awaiter is IConfiguredTaskAwaiter)) // TODO-NULLABLE: default(T) == null warning (https://github.com/dotnet/roslyn/issues/34757) - { - ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter ta = ref Unsafe.As(ref awaiter); - TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, ta.m_continueOnCapturedContext); - } - else if ((null != (object)default(TAwaiter)!) && (awaiter is IStateMachineBoxAwareAwaiter)) // TODO-NULLABLE: default(T) == null warning (https://github.com/dotnet/roslyn/issues/34757) - { - try - { - ((IStateMachineBoxAwareAwaiter)awaiter).AwaitUnsafeOnCompleted(box); - } - catch (Exception e) - { - // Whereas with Task the code that hooks up and invokes the continuation is all local to corelib, - // with ValueTaskAwaiter we may be calling out to an arbitrary implementation of IValueTaskSource - // wrapped in the ValueTask, and as such we protect against errant exceptions that may emerge. - // We don't want such exceptions propagating back into the async method, which can't handle - // exceptions well at that location in the state machine, especially if the exception may occur - // after the ValueTaskAwaiter already successfully hooked up the callback, in which case it's possible - // two different flows of execution could end up happening in the same async method call. - System.Threading.Tasks.Task.ThrowAsync(e, targetContext: null); - } - } - else - { - // The awaiter isn't specially known. Fall back to doing a normal await. - try - { - awaiter.UnsafeOnCompleted(box.MoveNextAction); - } - catch (Exception e) - { - System.Threading.Tasks.Task.ThrowAsync(e, targetContext: null); - } - } - } - - /// Gets the "boxed" state machine object. - /// Specifies the type of the async state machine. - /// The state machine. - /// The "boxed" state machine. - private IAsyncStateMachineBox GetStateMachineBox( - ref TStateMachine stateMachine) - where TStateMachine : IAsyncStateMachine - { - ExecutionContext? currentContext = ExecutionContext.Capture(); - - // Check first for the most common case: not the first yield in an async method. - // In this case, the first yield will have already "boxed" the state machine in - // a strongly-typed manner into an AsyncStateMachineBox. It will already contain - // the state machine as well as a MoveNextDelegate and a context. The only thing - // we might need to do is update the context if that's changed since it was stored. - if (m_task is AsyncStateMachineBox stronglyTypedBox) - { - if (stronglyTypedBox.Context != currentContext) - { - stronglyTypedBox.Context = currentContext; - } - return stronglyTypedBox; - } - - // The least common case: we have a weakly-typed boxed. This results if the debugger - // or some other use of reflection accesses a property like ObjectIdForDebugger or a - // method like SetNotificationForWaitCompletion prior to the first await happening. In - // such situations, we need to get an object to represent the builder, but we don't yet - // know the type of the state machine, and thus can't use TStateMachine. Instead, we - // use the IAsyncStateMachine interface, which all TStateMachines implement. This will - // result in a boxing allocation when storing the TStateMachine if it's a struct, but - // this only happens in active debugging scenarios where such performance impact doesn't - // matter. - if (m_task is AsyncStateMachineBox weaklyTypedBox) - { - // If this is the first await, we won't yet have a state machine, so store it. - if (weaklyTypedBox.StateMachine == null) - { - Debugger.NotifyOfCrossThreadDependency(); // same explanation as with usage below - weaklyTypedBox.StateMachine = stateMachine; - } - - // 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; - } - - // 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(); - - // At this point, m_task should really be null, in which case we want to create the box. - // However, in a variety of debugger-related (erroneous) situations, it might be non-null, - // e.g. if the Task property is examined in a Watch window, forcing it to be lazily-intialized - // as a Task rather than as an AsyncStateMachineBox. The worst that happens in such - // cases is we lose the ability to properly step in the debugger, as the debugger uses that - // object's identity to track this specific builder/state machine. As such, we proceed to - // overwrite whatever's there anyway, even if it's non-null. -#if CORERT - // DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because - // it will have a copy of all the slots from its parent. It will add another hundred(s) bytes - // per each async method in CoreRT / ProjectN binaries without adding much value. Avoid - // generating this extra code until a better solution is implemented. - var box = new AsyncStateMachineBox(); -#else - var box = AsyncMethodBuilderCore.TrackAsyncMethodCompletion ? - CreateDebugFinalizableAsyncStateMachineBox() : - new AsyncStateMachineBox(); -#endif - m_task = box; // important: this must be done before storing stateMachine into box.StateMachine! - box.StateMachine = stateMachine; - box.Context = currentContext; - - // Finally, log the creation of the state machine box object / task for this async method. - if (AsyncCausalityTracer.LoggingOn) - { - AsyncCausalityTracer.TraceOperationCreation(box, "Async: " + stateMachine.GetType().Name); - } - - return box; - } + => AsyncMethodBuilderCore.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref m_task); #if !CORERT // Avoid forcing the JIT to build DebugFinalizableAsyncStateMachineBox unless it's actually needed. [MethodImpl(MethodImplOptions.NoInlining)] - private static AsyncStateMachineBox CreateDebugFinalizableAsyncStateMachineBox() + internal static AsyncStateMachineBox CreateDebugFinalizableAsyncStateMachineBox() where TStateMachine : IAsyncStateMachine => new DebugFinalizableAsyncStateMachineBox(); @@ -557,7 +397,7 @@ private sealed class DebugFinalizableAsyncStateMachineBox : // SO /// A strongly-typed box for Task-based async state machines. /// Specifies the type of the state machine. - private class AsyncStateMachineBox : // SOS DumpAsync command depends on this name + internal class AsyncStateMachineBox : // SOS DumpAsync command depends on this name Task, IAsyncStateMachineBox where TStateMachine : IAsyncStateMachine { @@ -649,42 +489,7 @@ private void MoveNext(Thread? threadPoolThread) public Task Task { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => m_task ?? InitializeTaskAsPromise(); - } - - /// - /// Initializes the task, which must not yet be initialized. Used only when the Task is being forced into - /// existence when no state machine is needed, e.g. when the builder is being synchronously completed with - /// an exception, when the builder is being used out of the context of an async method, etc. - /// - [MethodImpl(MethodImplOptions.NoInlining)] - private Task InitializeTaskAsPromise() - { - Debug.Assert(m_task == null); - return (m_task = new Task()); - } - - /// - /// Initializes the task, which must not yet be initialized. Used only when the Task is being forced into - /// existence due to the debugger trying to enable step-out/step-over/etc. prior to the first await yielding - /// in an async method. In that case, we don't know the actual TStateMachine type, so we're forced to - /// use IAsyncStateMachine instead. - /// - [MethodImpl(MethodImplOptions.NoInlining)] - private Task InitializeTaskAsStateMachineBox() - { - Debug.Assert(m_task == null); -#if CORERT - // DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because - // it will have a copy of all the slots from its parent. It will add another hundred(s) bytes - // per each async method in CoreRT / ProjectN binaries without adding much value. Avoid - // generating this extra code until a better solution is implemented. - return (m_task = new AsyncStateMachineBox()); -#else - return (m_task = AsyncMethodBuilderCore.TrackAsyncMethodCompletion ? - CreateDebugFinalizableAsyncStateMachineBox() : - new AsyncStateMachineBox()); -#endif + get => m_task ?? AsyncMethodBuilderCore.InitializeTaskAsPromise(ref m_task!); // TODO-NULLABLE: Remove ! when nullable attributes are respected } /// @@ -694,78 +499,7 @@ private Task InitializeTaskAsStateMachineBox() /// The result to use to complete the task. /// The task has already completed. 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) - { - m_task = GetTaskForResult(result); - Debug.Assert(m_task != null, $"{nameof(GetTaskForResult)} should never return null"); - } - else - { - // Slow path: complete the existing task. - SetExistingTaskResult(result); - } - } - - /// Completes the already initialized task with the specified result. - /// The result to use to complete the task. - private void SetExistingTaskResult([AllowNull] TResult result) - { - Debug.Assert(m_task != null, "Expected non-null task"); - - if (AsyncCausalityTracer.LoggingOn || System.Threading.Tasks.Task.s_asyncDebuggingEnabled) - { - LogExistingTaskCompletion(); - } - - if (!m_task.TrySetResult(result)) - { - ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted); - } - } - - /// Handles logging for the successful completion of an operation. - private void LogExistingTaskCompletion() - { - Debug.Assert(m_task != null); - - if (AsyncCausalityTracer.LoggingOn) - { - AsyncCausalityTracer.TraceOperationCompletion(m_task, 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); - } - } - - /// - /// Completes the builder by using either the supplied completed task, or by completing - /// the builder's previously accessed task using default(TResult). - /// - /// A task already completed with the value default(TResult). - /// The task has already completed. - internal void SetResult(Task completedTask) - { - Debug.Assert(completedTask != null, "Expected non-null task"); - Debug.Assert(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) - { - m_task = completedTask; - } - else - { - // Otherwise, complete the task that's there. - SetExistingTaskResult(default!); // Remove ! when nullable attributes are respected - } - } + => AsyncMethodBuilderCore.SetResult(ref m_task, result); /// /// Completes the in the @@ -775,31 +509,7 @@ internal void SetResult(Task completedTask) /// The argument is null (Nothing in Visual Basic). /// The task has already completed. public void SetException(Exception exception) - { - if (exception == null) - { - ThrowHelper.ThrowArgumentNullException(ExceptionArgument.exception); - } - - // Get the task, forcing initialization if it hasn't already been initialized. - Task task = this.Task; - - // If the exception represents cancellation, cancel the task. Otherwise, fault the task. - bool successfullySet = exception is OperationCanceledException oce ? - task.TrySetCanceled(oce.CancellationToken, oce) : - task.TrySetException(exception!); // TODO-NULLABLE: Remove ! when [DoesNotReturn] respected - - // 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 - // return true, so no spinning would be necessary anyway (the spinning in TCS is only relevant - // if another thread completes the task first). - if (!successfullySet) - { - ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted); - } - } + => AsyncMethodBuilderCore.SetException(ref m_task, exception); /// /// Called by the debugger to request notification when the first wait operation @@ -813,20 +523,7 @@ public void SetException(Exception exception) /// and only by the debugger. /// internal void SetNotificationForWaitCompletion(bool enabled) - { - // Get the task (forcing initialization if not already initialized), and set debug notification - (m_task ?? InitializeTaskAsStateMachineBox()).SetNotificationForWaitCompletion(enabled); - - // NOTE: It's important that the debugger use builder.SetNotificationForWaitCompletion - // rather than builder.Task.SetNotificationForWaitCompletion. Even though the latter will - // lazily-initialize the task as well, it'll initialize it to a Task (which is important - // to minimize size for cases where an ATMB is used directly by user code to avoid the - // allocation overhead of a TaskCompletionSource). If that's done prior to the first await, - // the GetMoveNextDelegate code, which needs an AsyncStateMachineBox, will end up creating - // a new box and overwriting the previously created task. That'll change the object identity - // of the task being used for wait completion notification, and no notification will - // ever arrive, breaking step-out behavior when stepping out before the first yielding await. - } + => AsyncMethodBuilderCore.SetNotificationForWaitCompletion(ref m_task, enabled); /// /// Gets an object that may be used to uniquely identify this builder to the debugger. @@ -836,7 +533,15 @@ 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 or other members that lazily initialize the task. /// - internal object ObjectIdForDebugger => m_task ?? InitializeTaskAsStateMachineBox(); + internal object ObjectIdForDebugger => AsyncMethodBuilderCore.ObjectIdForDebugger(ref m_task); + } + + internal static class AsyncTaskCache + { +#if !PROJECTN + /// A cached task for default(TResult). + private readonly static Task s_defaultResultTask = AsyncTaskCache.CreateCacheableTask(default(TResult)!); // TODO-NULLABLE: Remove ! when nullable attributes are respected +#endif /// /// Gets a task for the specified result. This will either @@ -991,6 +696,323 @@ internal interface IAsyncStateMachineBox /// Shared helpers for manipulating state related to async state machines. internal static class AsyncMethodBuilderCore // debugger depends on this exact name { + /// + /// Completes the in the + /// Faulted state with the specified exception. + /// + /// Specifies the result type for the Task. + /// The to fault, or create faulted. + /// The to use to fault the task. + /// The argument is null (Nothing in Visual Basic). + /// The task has already completed. + public static void SetException([AllowNull] ref Task task, Exception exception) + { + if (exception == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.exception); + } + + // Forcing initialization of task if it hasn't already been initialized. + if (task is null) + { + InitializeTaskAsPromise(ref task!); // TODO-NULLABLE: Remove ! when nullable attributes are respected + } + + // If the exception represents cancellation, cancel the task. Otherwise, fault the task. + bool successfullySet = exception is OperationCanceledException oce ? + task!.TrySetCanceled(oce.CancellationToken, oce) : + task!.TrySetException(exception!); // TODO-NULLABLE: Remove ! when [DoesNotReturn] respected + + // 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 + // return true, so no spinning would be necessary anyway (the spinning in TCS is only relevant + // if another thread completes the task first). + if (!successfullySet) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted); + } + } + + /// + /// Initializes the task, which must not yet be initialized. Used only when the Task is being forced into + /// existence when no state machine is needed, e.g. when the builder is being synchronously completed with + /// an exception, when the builder is being used out of the context of an async method, etc. + /// + /// Specifies the result type for the Task. + /// The to create. + [MethodImpl(MethodImplOptions.NoInlining)] + public static Task InitializeTaskAsPromise([AllowNull] ref Task task) + { + Debug.Assert(task == null); + return (task = new Task()); + } + + /// + /// Schedules the specified state machine to be pushed forward when the specified awaiter completes. + /// + /// Specifies the result type for the Task. + /// Specifies the type of the awaiter. + /// Specifies the type of the state machine. + /// The awaiter. + /// The state machine. + /// The field to store the state. + // AggressiveOptimization to workaround boxing allocations in Tier0 until: https://github.com/dotnet/coreclr/issues/14474 + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + public static void AwaitUnsafeOnCompleted( + ref TAwaiter awaiter, ref TStateMachine stateMachine, ref Task task) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + { + IAsyncStateMachineBox box = GetStateMachineBox(ref task, ref stateMachine); + + // The null tests here ensure that the jit can optimize away the interface + // tests when TAwaiter is a ref type. + + if ((null != (object)default(TAwaiter)!) && (awaiter is ITaskAwaiter)) // TODO-NULLABLE: default(T) == null warning (https://github.com/dotnet/roslyn/issues/34757) + { + 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 ((null != (object)default(TAwaiter)!) && (awaiter is IConfiguredTaskAwaiter)) // TODO-NULLABLE: default(T) == null warning (https://github.com/dotnet/roslyn/issues/34757) + { + ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter ta = ref Unsafe.As(ref awaiter); + TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, ta.m_continueOnCapturedContext); + } + else if ((null != (object)default(TAwaiter)!) && (awaiter is IStateMachineBoxAwareAwaiter)) // TODO-NULLABLE: default(T) == null warning (https://github.com/dotnet/roslyn/issues/34757) + { + try + { + ((IStateMachineBoxAwareAwaiter)awaiter).AwaitUnsafeOnCompleted(box); + } + catch (Exception e) + { + // Whereas with Task the code that hooks up and invokes the continuation is all local to corelib, + // with ValueTaskAwaiter we may be calling out to an arbitrary implementation of IValueTaskSource + // wrapped in the ValueTask, and as such we protect against errant exceptions that may emerge. + // We don't want such exceptions propagating back into the async method, which can't handle + // exceptions well at that location in the state machine, especially if the exception may occur + // after the ValueTaskAwaiter already successfully hooked up the callback, in which case it's possible + // two different flows of execution could end up happening in the same async method call. + System.Threading.Tasks.Task.ThrowAsync(e, targetContext: null); + } + } + else + { + // The awaiter isn't specially known. Fall back to doing a normal await. + try + { + awaiter.UnsafeOnCompleted(box.MoveNextAction); + } + catch (Exception e) + { + System.Threading.Tasks.Task.ThrowAsync(e, targetContext: null); + } + } + } + + /// + /// Schedules the specified state machine to be pushed forward when the specified awaiter completes. + /// + /// Specifies the result type for the Task. + /// Specifies the type of the awaiter. + /// Specifies the type of the state machine. + /// The awaiter. + /// The state machine. + /// The field to store the state. + public static void AwaitOnCompleted( + ref TAwaiter awaiter, ref TStateMachine stateMachine, ref Task task) + where TAwaiter : INotifyCompletion + where TStateMachine : IAsyncStateMachine + { + try + { + awaiter.OnCompleted(GetStateMachineBox(ref task, ref stateMachine).MoveNextAction); + } + catch (Exception e) + { + System.Threading.Tasks.Task.ThrowAsync(e, targetContext: null); + } + } + + /// + /// Completes the builder by using either the supplied completed task, or by completing + /// the builder's previously accessed task using default(TResult). + /// + /// Specifies the result type for the Task. + /// The field to set the result for. + /// A task already completed with the value default(TResult). + /// The task has already completed. + internal static void SetResult([AllowNull] ref Task task, Task completedTask) + { + Debug.Assert(completedTask != null, "Expected non-null task"); + Debug.Assert(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 (task == null) + { + task = completedTask; + } + else + { + // Otherwise, complete the task that's there. + SetExistingTaskResult(task, default!); // Remove ! when nullable attributes are respected + } + } + + /// + /// Completes the in the + /// RanToCompletion state with the specified result. + /// + /// Specifies the result type for the Task. + /// The field to set the result for. + /// The result to use to complete the task. + /// The task has already completed. + public static void SetResult([AllowNull] ref Task task, 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 (task == null) + { + task = AsyncTaskCache.GetTaskForResult(result); + Debug.Assert(task != null, $"{nameof(AsyncTaskCache.GetTaskForResult)} should never return null"); + } + else + { + // Slow path: complete the existing task. + SetExistingTaskResult(task, result); + } + } + + /// Completes the already initialized task with the specified result. + /// Specifies the result type for the Task. + /// The to set the result of. + /// The result to use to complete the task. + public static void SetExistingTaskResult(Task task, [AllowNull] TResult result) + { + Debug.Assert(task != null, "Expected non-null task"); + + if (AsyncCausalityTracer.LoggingOn || System.Threading.Tasks.Task.s_asyncDebuggingEnabled) + { + LogExistingTaskCompletion(task); + } + + if (!task.TrySetResult(result)) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted); + } + } + + /// Handles logging for the successful completion of an operation. + /// The to log completion of. + private static void LogExistingTaskCompletion(Task task) + { + Debug.Assert(task != null); + + if (AsyncCausalityTracer.LoggingOn) + { + AsyncCausalityTracer.TraceOperationCompletion(task, 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(task); + } + } + + /// Gets the "boxed" state machine object. + /// Specifies the result type for the Task. + /// Specifies the type of the async state machine. + /// The field to get or set the statemachine of. + /// The state machine. + /// The "boxed" state machine. + public static IAsyncStateMachineBox GetStateMachineBox( + ref Task task, + ref TStateMachine stateMachine) + where TStateMachine : IAsyncStateMachine + { + ExecutionContext? currentContext = ExecutionContext.Capture(); + + // Check first for the most common case: not the first yield in an async method. + // In this case, the first yield will have already "boxed" the state machine in + // a strongly-typed manner into an AsyncStateMachineBox. It will already contain + // the state machine as well as a MoveNextDelegate and a context. The only thing + // we might need to do is update the context if that's changed since it was stored. + if (task is AsyncTaskMethodBuilder.AsyncStateMachineBox stronglyTypedBox) + { + if (stronglyTypedBox.Context != currentContext) + { + stronglyTypedBox.Context = currentContext; + } + return stronglyTypedBox; + } + + // The least common case: we have a weakly-typed boxed. This results if the debugger + // or some other use of reflection accesses a property like ObjectIdForDebugger or a + // method like SetNotificationForWaitCompletion prior to the first await happening. In + // such situations, we need to get an object to represent the builder, but we don't yet + // know the type of the state machine, and thus can't use TStateMachine. Instead, we + // use the IAsyncStateMachine interface, which all TStateMachines implement. This will + // result in a boxing allocation when storing the TStateMachine if it's a struct, but + // this only happens in active debugging scenarios where such performance impact doesn't + // matter. + if (task is AsyncTaskMethodBuilder.AsyncStateMachineBox weaklyTypedBox) + { + // If this is the first await, we won't yet have a state machine, so store it. + if (weaklyTypedBox.StateMachine == null) + { + Debugger.NotifyOfCrossThreadDependency(); // same explanation as with usage below + weaklyTypedBox.StateMachine = stateMachine; + } + + // 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; + } + + // 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(); + + // At this point, m_task should really be null, in which case we want to create the box. + // However, in a variety of debugger-related (erroneous) situations, it might be non-null, + // e.g. if the Task property is examined in a Watch window, forcing it to be lazily-intialized + // as a Task rather than as an AsyncStateMachineBox. The worst that happens in such + // cases is we lose the ability to properly step in the debugger, as the debugger uses that + // object's identity to track this specific builder/state machine. As such, we proceed to + // overwrite whatever's there anyway, even if it's non-null. +#if CORERT + // DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because + // it will have a copy of all the slots from its parent. It will add another hundred(s) bytes + // per each async method in CoreRT / ProjectN binaries without adding much value. Avoid + // generating this extra code until a better solution is implemented. + var box = new AsyncTaskMethodBuilder.AsyncStateMachineBox(); +#else + var box = TrackAsyncMethodCompletion ? + AsyncTaskMethodBuilder.CreateDebugFinalizableAsyncStateMachineBox() : + new AsyncTaskMethodBuilder.AsyncStateMachineBox(); +#endif + task = box; // important: this must be done before storing stateMachine into box.StateMachine! + box.StateMachine = stateMachine; + box.Context = currentContext; + + // Finally, log the creation of the state machine box object / task for this async method. + if (AsyncCausalityTracer.LoggingOn) + { + AsyncCausalityTracer.TraceOperationCreation(box, "Async: " + stateMachine.GetType().Name); + } + + return box; + } + /// Initiates the builder's execution with the associated state machine. /// Specifies the type of the state machine. /// The state machine instance, passed by reference. @@ -1105,6 +1127,86 @@ internal static string GetAsyncStateMachineDescription(IAsyncStateMachine stateM internal static Task? TryGetContinuationTask(Action continuation) => (continuation?.Target as ContinuationWrapper)?._innerTask; + /// + /// Gets an object that may be used to uniquely identify this builder to the debugger. + /// + /// Specifies the result type for the Task. + /// The field to get the ObjectId for. + /// + /// This property lazily instantiates the ID in a non-thread-safe manner. + /// 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 or other members that lazily initialize the task. + /// + internal static object ObjectIdForDebugger(ref Task task) => task ?? InitializeTaskAsStateMachineBox(ref task!); // TODO-NULLABLE: Remove ! when nullable attributes are respected + + /// + /// Called by the debugger to request notification when the first wait operation + /// (await, Wait, Result, etc.) on this builder's task completes. + /// + /// Specifies the result type for the Task. + /// The field to get set the notification for. + /// + /// true to enable notification; false to disable a previously set notification. + /// + /// + /// This should only be invoked from within an asynchronous method, + /// and only by the debugger. + /// + internal static void SetNotificationForWaitCompletion(ref Task task, bool enabled) + { + // Get the task (forcing initialization if not already initialized), and set debug notification + (task ?? InitializeTaskAsStateMachineBox(ref task!)).SetNotificationForWaitCompletion(enabled); // TODO-NULLABLE: Remove ! when nullable attributes are respected + + // NOTE: It's important that the debugger use builder.SetNotificationForWaitCompletion + // rather than builder.Task.SetNotificationForWaitCompletion. Even though the latter will + // lazily-initialize the task as well, it'll initialize it to a Task (which is important + // to minimize size for cases where an ATMB is used directly by user code to avoid the + // allocation overhead of a TaskCompletionSource). If that's done prior to the first await, + // the GetMoveNextDelegate code, which needs an AsyncStateMachineBox, will end up creating + // a new box and overwriting the previously created task. That'll change the object identity + // of the task being used for wait completion notification, and no notification will + // ever arrive, breaking step-out behavior when stepping out before the first yielding await. + } + +#if PROJECTN + public static TMethodBuilder InitalizeTaskIfDebugging(ref TMethodBuilder methodBuilder, [AllowNull] ref Task task) + { + // This allows the debugger to access m_task directly without evaluating ObjectIdForDebugger for ProjectN + if (Task.s_asyncDebuggingEnabled) + { + // This allows the debugger to access m_task directly without evaluating ObjectIdForDebugger for ProjectN + InitializeTaskAsStateMachineBox(ref task); + } + + return methodBuilder; + } +#endif + + /// + /// Initializes the task, which must not yet be initialized. Used only when the Task is being forced into + /// existence due to the debugger trying to enable step-out/step-over/etc. prior to the first await yielding + /// in an async method. In that case, we don't know the actual TStateMachine type, so we're forced to + /// use IAsyncStateMachine instead. + /// + /// Specifies the result type for the Task. + /// The field to initialize. + [MethodImpl(MethodImplOptions.NoInlining)] + internal static Task InitializeTaskAsStateMachineBox([AllowNull] ref Task task) + { + Debug.Assert(task == null); +#if CORERT + // DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because + // it will have a copy of all the slots from its parent. It will add another hundred(s) bytes + // per each async method in CoreRT / ProjectN binaries without adding much value. Avoid + // generating this extra code until a better solution is implemented. + return (task = new AsyncTaskMethodBuilder.AsyncStateMachineBox()); +#else + return (task = TrackAsyncMethodCompletion ? + AsyncTaskMethodBuilder.CreateDebugFinalizableAsyncStateMachineBox() : + new AsyncTaskMethodBuilder.AsyncStateMachineBox()); +#endif + } + /// /// 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 diff --git a/src/System.Private.CoreLib/shared/System/Runtime/CompilerServices/AsyncValueTaskMethodBuilder.cs b/src/System.Private.CoreLib/shared/System/Runtime/CompilerServices/AsyncValueTaskMethodBuilder.cs index 897de6e27db9..74f09c70f4e4 100644 --- a/src/System.Private.CoreLib/shared/System/Runtime/CompilerServices/AsyncValueTaskMethodBuilder.cs +++ b/src/System.Private.CoreLib/shared/System/Runtime/CompilerServices/AsyncValueTaskMethodBuilder.cs @@ -12,25 +12,20 @@ namespace System.Runtime.CompilerServices [StructLayout(LayoutKind.Auto)] public struct AsyncValueTaskMethodBuilder { - /// The to which most operations are delegated. - private AsyncTaskMethodBuilder _methodBuilder; // mutable struct; do not make it readonly - /// true if completed synchronously and successfully; otherwise, false. - private bool _haveResult; - /// true if the builder should be used for setting/getting the result; otherwise, false. - private bool _useBuilder; + /// The lazily-initialized built task. + private Task m_task; // Debugger depends on the exact name of this field. /// Creates an instance of the struct. /// The initialized instance. - public static AsyncValueTaskMethodBuilder Create() => + public static AsyncValueTaskMethodBuilder Create() + { #if PROJECTN - // ProjectN's AsyncTaskMethodBuilder.Create() currently does additional debugger-related - // work, so we need to delegate to it. - new AsyncValueTaskMethodBuilder() { _methodBuilder = AsyncTaskMethodBuilder.Create() }; + var result = new AsyncValueTaskMethodBuilder(); + return AsyncMethodBuilderCore.InitalizeTaskIfDebugging(ref result, ref result.m_task!); // TODO-NULLABLE: Remove ! when nullable attributes are respected #else - // _methodBuilder should be initialized to AsyncTaskMethodBuilder.Create(), but on coreclr - // that Create() is a nop, so we can just return the default here. - default; + return default; #endif + } /// Begins running the builder with the associated state machine. /// The type of the state machine. @@ -42,42 +37,41 @@ public struct AsyncValueTaskMethodBuilder /// Associates the builder with the specified state machine. /// The state machine instance to associate with the builder. - public void SetStateMachine(IAsyncStateMachine stateMachine) => _methodBuilder.SetStateMachine(stateMachine); + public void SetStateMachine(IAsyncStateMachine stateMachine) + => AsyncMethodBuilderCore.SetStateMachine(stateMachine, m_task); /// Marks the task as successfully completed. public void SetResult() { - if (_useBuilder) + if (m_task is null) { - _methodBuilder.SetResult(); + m_task = System.Threading.Tasks.Task.s_cachedCompleted; } else { - _haveResult = true; + AsyncMethodBuilderCore.SetExistingTaskResult(m_task, default); } } /// Marks the task as failed and binds the specified exception to the task. /// The exception to bind to the task. - public void SetException(Exception exception) => _methodBuilder.SetException(exception); + public void SetException(Exception exception) + => AsyncMethodBuilderCore.SetException(ref m_task, exception); /// Gets the task for this builder. public ValueTask Task { get { - if (_haveResult) - { - return default; - } - else - { - _useBuilder = true; - return new ValueTask(_methodBuilder.Task); - } + return ReferenceEquals(m_task, System.Threading.Tasks.Task.s_cachedCompleted) ? + default : + CreateValueTask(ref m_task); } } + [MethodImpl(MethodImplOptions.NoInlining)] + private static ValueTask CreateValueTask(ref Task task) => new ValueTask(task ?? AsyncMethodBuilderCore.InitializeTaskAsPromise(ref task!)); // TODO-NULLABLE: Remove ! when nullable attributes are respected + /// Schedules the state machine to proceed to the next action when the specified awaiter completes. /// The type of the awaiter. /// The type of the state machine. @@ -86,10 +80,7 @@ public ValueTask Task public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine - { - _useBuilder = true; - _methodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine); - } + => AsyncMethodBuilderCore.AwaitOnCompleted(ref awaiter, ref stateMachine, ref m_task); /// Schedules the state machine to proceed to the next action when the specified awaiter completes. /// The type of the awaiter. @@ -99,10 +90,7 @@ public ValueTask Task public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine - { - _useBuilder = true; - _methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); - } + => AsyncMethodBuilderCore.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref m_task); } /// Represents a builder for asynchronous methods that returns a . @@ -110,27 +98,24 @@ public ValueTask Task [StructLayout(LayoutKind.Auto)] public struct AsyncValueTaskMethodBuilder { - /// The to which most operations are delegated. - private AsyncTaskMethodBuilder _methodBuilder; // mutable struct; do not make it readonly + /// used if contains the synchronous result for the async method. + private static readonly Task s_haveResultSentinel = new Task(); + + private Task m_task; // Debugger depends on the exact name of this field. /// The result for this builder, if it's completed before any awaits occur. private TResult _result; - /// true if contains the synchronous result for the async method; otherwise, false. - private bool _haveResult; - /// true if the builder should be used for setting/getting the result; otherwise, false. - private bool _useBuilder; /// Creates an instance of the struct. /// The initialized instance. - public static AsyncValueTaskMethodBuilder Create() => + public static AsyncValueTaskMethodBuilder Create() + { #if PROJECTN - // ProjectN's AsyncTaskMethodBuilder.Create() currently does additional debugger-related - // work, so we need to delegate to it. - new AsyncValueTaskMethodBuilder() { _methodBuilder = AsyncTaskMethodBuilder.Create() }; + var result = new AsyncValueTaskMethodBuilder(); + return AsyncMethodBuilderCore.InitalizeTaskIfDebugging(ref result, ref result.m_task!); // TODO-NULLABLE: Remove ! when nullable attributes are respected #else - // _methodBuilder should be initialized to AsyncTaskMethodBuilder.Create(), but on coreclr - // that Create() is a nop, so we can just return the default here. - default; + return default; #endif + } /// Begins running the builder with the associated state machine. /// The type of the state machine. @@ -142,44 +127,43 @@ public struct AsyncValueTaskMethodBuilder /// Associates the builder with the specified state machine. /// The state machine instance to associate with the builder. - public void SetStateMachine(IAsyncStateMachine stateMachine) => _methodBuilder.SetStateMachine(stateMachine); + public void SetStateMachine(IAsyncStateMachine stateMachine) + => AsyncMethodBuilderCore.SetStateMachine(stateMachine, m_task); /// Marks the task as successfully completed. /// The result to use to complete the task. public void SetResult(TResult result) { - if (_useBuilder) + if (m_task is null) { - _methodBuilder.SetResult(result); + _result = result; + m_task = s_haveResultSentinel; } else { - _result = result; - _haveResult = true; + AsyncMethodBuilderCore.SetExistingTaskResult(m_task, result); } } /// Marks the task as failed and binds the specified exception to the task. /// The exception to bind to the task. - public void SetException(Exception exception) => _methodBuilder.SetException(exception); + public void SetException(Exception exception) + => AsyncMethodBuilderCore.SetException(ref m_task, exception); /// Gets the task for this builder. public ValueTask Task { get { - if (_haveResult) - { - return new ValueTask(_result); - } - else - { - _useBuilder = true; - return new ValueTask(_methodBuilder.Task); - } + return ReferenceEquals(s_haveResultSentinel, m_task) ? + new ValueTask(_result) : + CreateValueTask(ref m_task); } } + [MethodImpl(MethodImplOptions.NoInlining)] + private static ValueTask CreateValueTask(ref Task task) => new ValueTask(task ?? AsyncMethodBuilderCore.InitializeTaskAsPromise(ref task!)); // TODO-NULLABLE: Remove ! when nullable attributes are respected + /// Schedules the state machine to proceed to the next action when the specified awaiter completes. /// The type of the awaiter. /// The type of the state machine. @@ -188,10 +172,7 @@ public ValueTask Task public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine - { - _useBuilder = true; - _methodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine); - } + => AsyncMethodBuilderCore.AwaitOnCompleted(ref awaiter, ref stateMachine, ref m_task); /// Schedules the state machine to proceed to the next action when the specified awaiter completes. /// The type of the awaiter. @@ -201,9 +182,6 @@ public ValueTask Task public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine - { - _useBuilder = true; - _methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); - } + => AsyncMethodBuilderCore.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref m_task); } } diff --git a/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs b/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs index 135f5883f38a..87be39d735f7 100644 --- a/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs +++ b/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs @@ -1483,8 +1483,11 @@ bool IAsyncResult.CompletedSynchronously /// public static TaskFactory Factory { get; } = new TaskFactory(); + // Is a Task{VoidTaskResult} so it can be shared with AsyncTaskMethodBuilder + internal static readonly Task s_cachedCompleted = new Task(false, default, (TaskCreationOptions)InternalTaskOptions.DoNotDispose, default); + /// Gets a task that's already been completed successfully. - public static Task CompletedTask { get; } = new Task(false, (TaskCreationOptions)InternalTaskOptions.DoNotDispose, default); + public static Task CompletedTask => s_cachedCompleted; /// /// Provides an event that can be used to wait for completion. diff --git a/src/System.Private.CoreLib/shared/System/Threading/Tasks/ValueTask.cs b/src/System.Private.CoreLib/shared/System/Threading/Tasks/ValueTask.cs index 75c6fd9a325c..f32407dde31e 100644 --- a/src/System.Private.CoreLib/shared/System/Threading/Tasks/ValueTask.cs +++ b/src/System.Private.CoreLib/shared/System/Threading/Tasks/ValueTask.cs @@ -527,7 +527,7 @@ public Task AsTask() if (obj == null) { - return AsyncTaskMethodBuilder.GetTaskForResult(_result); + return AsyncTaskCache.GetTaskForResult(_result); } if (obj is Task t) @@ -555,7 +555,7 @@ private Task GetTaskForValueTaskSource(IValueTaskSource t) { // Get the result of the operation and return a task for it. // If any exception occurred, propagate it - return AsyncTaskMethodBuilder.GetTaskForResult(t.GetResult(_token)); + return AsyncTaskCache.GetTaskForResult(t.GetResult(_token)); // If status is Faulted or Canceled, GetResult should throw. But // we can't guarantee every implementation will do the "right thing".