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".