From c76a598cef4a19545184bbd75b483e823ae57fd2 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Sat, 9 May 2026 01:42:54 +0200 Subject: [PATCH 1/6] Special case runtime async in `ThreadPool.UnsafeQueueUserWorkItem` When user code forwards continuations passed to `IValueTaskSource.OnCompleted` to the thread pool we can avoid allocating a new work item. This function already has a special case that implements this optimization for async1. Add one for runtime async too. --- .../CompilerServices/AsyncHelpers.CoreCLR.cs | 10 ++----- .../System/Threading/ThreadPoolWorkQueue.cs | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs index bb068f1290c0f8..f1b66020a5efae 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs @@ -374,6 +374,8 @@ public RuntimeAsyncTask() m_stateFlags |= (int)InternalTaskOptions.HiddenState; } + // Note that ThreadPool.s_dispatchRuntimeAsyncContinuationsCallback + // calls this function and always passes null for the thread. internal override void ExecuteFromThreadPool(Thread threadPoolThread) { DispatchContinuations(); @@ -491,7 +493,7 @@ internal unsafe bool HandleSuspended(ref RuntimeAsyncAwaitState state) // Clear continuation flags, so that continuation runs transparently nextUserContinuation.Flags &= ~continueFlags; - valueTaskSourceNotifier.OnCompleted(s_runContinuationAction, this, configFlags); + valueTaskSourceNotifier.OnCompleted(ThreadPool.s_dispatchRuntimeAsyncContinuationsCallback, this, configFlags); } else { @@ -851,12 +853,6 @@ private bool QueueContinuationFollowUpActionIfNecessary(Continuation continuatio Debug.Assert(state is RuntimeAsyncTask); ((RuntimeAsyncTask)state).DispatchContinuations(); }; - - private static readonly Action s_runContinuationAction = static state => - { - Debug.Assert(state is RuntimeAsyncTask); - ((RuntimeAsyncTask)state).DispatchContinuations(); - }; } private static void InstrumentedFinalizeRuntimeAsyncTask(RuntimeAsyncTask task, ref RuntimeAsyncAwaitState state, AsyncInstrumentation.Flags flags) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs index c994f3fbcc3f0d..e219cc375c9a12 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs @@ -1428,6 +1428,20 @@ public static partial class ThreadPool } }; + internal static readonly Action s_dispatchRuntimeAsyncContinuationsCallback = static state => + { + if (state is Task t) + { + // We know RuntimeAsyncTask overrides this and calls + // DispatchContinuation without looking at the Thread. + t.ExecuteFromThreadPool(null!); + } + else + { + ThrowHelper.ThrowUnexpectedStateForKnownCallback(state); + } + }; + internal static bool EnableWorkerTracking => IsWorkerTrackingEnabledInConfig && EventSource.IsSupported; #if !FEATURE_WASM_MANAGED_THREADS @@ -1647,6 +1661,20 @@ public static bool UnsafeQueueUserWorkItem(Action callBack, TSta return true; } + // Similarly, for runtime async, user code may call with the + // runtime async callback directly. + if (ReferenceEquals(callBack, AsyncHelpers.s_dispatchRuntimeAsyncContinuationsCallback)) + { + if (state is not Task) + { + // The provided state must be the internal RuntimeAsyncTask (Task) + ThrowHelper.ThrowUnexpectedStateForKnownCallback(state); + } + + UnsafeQueueUserWorkItemInternal((object)state, preferLocal); + return true; + } + s_workQueue.Enqueue( new QueueUserWorkItemCallbackDefaultContext(callBack, state), forceGlobal: !preferLocal); From c58b9325ee0c293cf00e3a63d6aedd77b88c67ef Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Sat, 9 May 2026 02:03:54 +0200 Subject: [PATCH 2/6] Refactor --- .../src/System/Threading/ThreadPoolWorkQueue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs index e219cc375c9a12..153d331622beca 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs @@ -1663,7 +1663,7 @@ public static bool UnsafeQueueUserWorkItem(Action callBack, TSta // Similarly, for runtime async, user code may call with the // runtime async callback directly. - if (ReferenceEquals(callBack, AsyncHelpers.s_dispatchRuntimeAsyncContinuationsCallback)) + if (ReferenceEquals(callBack, s_dispatchRuntimeAsyncContinuationsCallback)) { if (state is not Task) { From 05e8b12cf53de80fa3d1129c9d5461af4d3c0ca9 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Sat, 9 May 2026 02:12:21 +0200 Subject: [PATCH 3/6] Feedback --- .../src/System/Threading/ThreadPoolWorkQueue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs index 153d331622beca..947968f05e245d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs @@ -1433,7 +1433,7 @@ public static partial class ThreadPool if (state is Task t) { // We know RuntimeAsyncTask overrides this and calls - // DispatchContinuation without looking at the Thread. + // DispatchContinuations without looking at the Thread. t.ExecuteFromThreadPool(null!); } else From 164e15e5fa3d4f1851f71c9c05450aef6fc4900f Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Mon, 11 May 2026 13:59:34 +0200 Subject: [PATCH 4/6] Only one delegate --- .../CompilerServices/AsyncHelpers.CoreCLR.cs | 4 +- .../AsyncTaskMethodBuilderT.cs | 2 +- .../ConfiguredValueTaskAwaitable.cs | 4 +- .../CompilerServices/ValueTaskAwaiter.cs | 4 +- .../src/System/Threading/Tasks/Task.cs | 11 ++-- .../System/Threading/ThreadPoolWorkQueue.cs | 50 +++++-------------- 6 files changed, 26 insertions(+), 49 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs index f1b66020a5efae..e5ef74b62b10b3 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs @@ -376,7 +376,7 @@ public RuntimeAsyncTask() // Note that ThreadPool.s_dispatchRuntimeAsyncContinuationsCallback // calls this function and always passes null for the thread. - internal override void ExecuteFromThreadPool(Thread threadPoolThread) + internal override void ExecuteDirectly(Thread? threadPoolThread) { DispatchContinuations(); } @@ -493,7 +493,7 @@ internal unsafe bool HandleSuspended(ref RuntimeAsyncAwaitState state) // Clear continuation flags, so that continuation runs transparently nextUserContinuation.Flags &= ~continueFlags; - valueTaskSourceNotifier.OnCompleted(ThreadPool.s_dispatchRuntimeAsyncContinuationsCallback, this, configFlags); + valueTaskSourceNotifier.OnCompleted(ThreadPool.s_invokeAsyncTask, this, configFlags); } else { diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.cs index 500a28a8d5952a..8d3e233865c558 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.cs @@ -344,7 +344,7 @@ public ref ExecutionContext? Context } } - internal sealed override void ExecuteFromThreadPool(Thread threadPoolThread) => MoveNext(threadPoolThread); + internal sealed override void ExecuteDirectly(Thread? threadPoolThread) => MoveNext(threadPoolThread); /// Calls MoveNext on public void MoveNext() => MoveNext(threadPoolThread: null); diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs index 1700fb0cca8aa6..ff9671cc42d715 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs @@ -102,7 +102,7 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b } else if (obj != null) { - Unsafe.As(obj).OnCompleted(ThreadPool.s_invokeAsyncStateMachineBox, box, _value._token, + Unsafe.As(obj).OnCompleted(ThreadPool.s_invokeAsyncTask, box, _value._token, _value._continueOnCapturedContext ? ValueTaskSourceOnCompletedFlags.UseSchedulingContext : ValueTaskSourceOnCompletedFlags.None); } else @@ -207,7 +207,7 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b } else if (obj != null) { - Unsafe.As>(obj).OnCompleted(ThreadPool.s_invokeAsyncStateMachineBox, box, _value._token, + Unsafe.As>(obj).OnCompleted(ThreadPool.s_invokeAsyncTask, box, _value._token, _value._continueOnCapturedContext ? ValueTaskSourceOnCompletedFlags.UseSchedulingContext : ValueTaskSourceOnCompletedFlags.None); } else diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.cs index c242ad3f2ad203..da632aa9e31f36 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.cs @@ -94,7 +94,7 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b } else if (obj != null) { - Unsafe.As(obj).OnCompleted(ThreadPool.s_invokeAsyncStateMachineBox, box, _value._token, ValueTaskSourceOnCompletedFlags.UseSchedulingContext); + Unsafe.As(obj).OnCompleted(ThreadPool.s_invokeAsyncTask, box, _value._token, ValueTaskSourceOnCompletedFlags.UseSchedulingContext); } else { @@ -176,7 +176,7 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b } else if (obj != null) { - Unsafe.As>(obj).OnCompleted(ThreadPool.s_invokeAsyncStateMachineBox, box, _value._token, ValueTaskSourceOnCompletedFlags.UseSchedulingContext); + Unsafe.As>(obj).OnCompleted(ThreadPool.s_invokeAsyncTask, box, _value._token, ValueTaskSourceOnCompletedFlags.UseSchedulingContext); } else { diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs index 1a559e653c7049..b486dc171a8764 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @@ -2423,12 +2423,13 @@ internal bool ExecuteEntry() } /// - /// ThreadPool's entry point into the Task. The base behavior is simply to - /// use the entry point that's not protected from double-invoke; derived internal tasks - /// can override to customize their behavior, which is usually done by promises - /// that want to reuse the same object as a queued work item. + /// This is used internally to execute the Task directly. ThreadPool uses this, + /// and it is also used to invoke async state machines and runtime async tasks directly. + /// The base behavior is simply to use the entry point that's not protected from + /// double-invoke; derived internal tasks can override to customize their behavior, + /// which is usually done by promises that want to reuse the same object as a queued work item. /// - internal virtual void ExecuteFromThreadPool(Thread threadPoolThread) => ExecuteEntryUnsafe(threadPoolThread); + internal virtual void ExecuteDirectly(Thread? threadPoolThread) => ExecuteEntryUnsafe(threadPoolThread); internal void ExecuteEntryUnsafe(Thread? threadPoolThread) // used instead of ExecuteEntry() when we don't have to worry about double-execution prevent { diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs index 947968f05e245d..2967083271243a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs @@ -1047,7 +1047,7 @@ private static void DispatchWorkItem(object workItem, Thread currentThread) { // Task workitems catch their exceptions for later observation // We do not need to pass unhandled ones to ExceptionHandling.s_handler - task.ExecuteFromThreadPool(currentThread); + task.ExecuteDirectly(currentThread); } else { @@ -1415,26 +1415,16 @@ public static partial class ThreadPool internal static readonly ThreadPoolWorkQueue s_workQueue = new ThreadPoolWorkQueue(); - /// Shim used to invoke of the supplied . - internal static readonly Action s_invokeAsyncStateMachineBox = static state => + /// Shim used to invoke async tasks or non-task IAsyncStateMachineBoxes. + internal static readonly Action s_invokeAsyncTask = static state => { - if (state is IAsyncStateMachineBox box) - { - box.MoveNext(); - } - else + if (state is Task t) { - ThrowHelper.ThrowUnexpectedStateForKnownCallback(state); + t.ExecuteDirectly(null); } - }; - - internal static readonly Action s_dispatchRuntimeAsyncContinuationsCallback = static state => - { - if (state is Task t) + else if (state is IAsyncStateMachineBox box) { - // We know RuntimeAsyncTask overrides this and calls - // DispatchContinuations without looking at the Thread. - t.ExecuteFromThreadPool(null!); + box.MoveNext(); } else { @@ -1643,15 +1633,15 @@ public static bool UnsafeQueueUserWorkItem(Action callBack, TSta ThrowHelper.ThrowArgumentNullException(ExceptionArgument.callBack); } - // If the callback is the runtime-provided invocation of an IAsyncStateMachineBox, - // then we can queue the Task state directly to the ThreadPool instead of - // wrapping it in a QueueUserWorkItemCallback. + // If the callback is the runtime-provided invocation of an async task (AsyncStateMachineBox, RuntimeAsyncTask) + // or non-task IAsyncStateMachineBox (PoolingAsyncValueTaskMethodBuilder.StateMachineBox), then we can queue + // its state directly to the ThreadPool instead of wrapping it in a QueueUserWorkItemCallback. // // This occurs when user code queues its provided continuation to the ThreadPool; - // internally we call UnsafeQueueUserWorkItemInternal directly for Tasks. - if (ReferenceEquals(callBack, s_invokeAsyncStateMachineBox)) + // internally we call UnsafeQueueUserWorkItemInternal directly for these cases, whenever possible. + if (ReferenceEquals(callBack, s_invokeAsyncTask)) { - if (state is not IAsyncStateMachineBox) + if (state is not Task && state is not IAsyncStateMachineBox) { // The provided state must be the internal IAsyncStateMachineBox (Task) type ThrowHelper.ThrowUnexpectedStateForKnownCallback(state); @@ -1661,20 +1651,6 @@ public static bool UnsafeQueueUserWorkItem(Action callBack, TSta return true; } - // Similarly, for runtime async, user code may call with the - // runtime async callback directly. - if (ReferenceEquals(callBack, s_dispatchRuntimeAsyncContinuationsCallback)) - { - if (state is not Task) - { - // The provided state must be the internal RuntimeAsyncTask (Task) - ThrowHelper.ThrowUnexpectedStateForKnownCallback(state); - } - - UnsafeQueueUserWorkItemInternal((object)state, preferLocal); - return true; - } - s_workQueue.Enqueue( new QueueUserWorkItemCallbackDefaultContext(callBack, state), forceGlobal: !preferLocal); From 9967c4f0533f4dd396ac2927e655be8c78b2edb6 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Mon, 11 May 2026 14:00:29 +0200 Subject: [PATCH 5/6] Partially revert last commit --- .../CompilerServices/AsyncHelpers.CoreCLR.cs | 2 +- .../ConfiguredValueTaskAwaitable.cs | 4 +- .../CompilerServices/ValueTaskAwaiter.cs | 4 +- .../System/Threading/ThreadPoolWorkQueue.cs | 48 ++++++++++++++----- 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs index e5ef74b62b10b3..d8a3a27188005e 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs @@ -493,7 +493,7 @@ internal unsafe bool HandleSuspended(ref RuntimeAsyncAwaitState state) // Clear continuation flags, so that continuation runs transparently nextUserContinuation.Flags &= ~continueFlags; - valueTaskSourceNotifier.OnCompleted(ThreadPool.s_invokeAsyncTask, this, configFlags); + valueTaskSourceNotifier.OnCompleted(ThreadPool.s_dispatchRuntimeAsyncContinuationsCallback, this, configFlags); } else { diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs index ff9671cc42d715..1700fb0cca8aa6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs @@ -102,7 +102,7 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b } else if (obj != null) { - Unsafe.As(obj).OnCompleted(ThreadPool.s_invokeAsyncTask, box, _value._token, + Unsafe.As(obj).OnCompleted(ThreadPool.s_invokeAsyncStateMachineBox, box, _value._token, _value._continueOnCapturedContext ? ValueTaskSourceOnCompletedFlags.UseSchedulingContext : ValueTaskSourceOnCompletedFlags.None); } else @@ -207,7 +207,7 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b } else if (obj != null) { - Unsafe.As>(obj).OnCompleted(ThreadPool.s_invokeAsyncTask, box, _value._token, + Unsafe.As>(obj).OnCompleted(ThreadPool.s_invokeAsyncStateMachineBox, box, _value._token, _value._continueOnCapturedContext ? ValueTaskSourceOnCompletedFlags.UseSchedulingContext : ValueTaskSourceOnCompletedFlags.None); } else diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.cs index da632aa9e31f36..c242ad3f2ad203 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.cs @@ -94,7 +94,7 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b } else if (obj != null) { - Unsafe.As(obj).OnCompleted(ThreadPool.s_invokeAsyncTask, box, _value._token, ValueTaskSourceOnCompletedFlags.UseSchedulingContext); + Unsafe.As(obj).OnCompleted(ThreadPool.s_invokeAsyncStateMachineBox, box, _value._token, ValueTaskSourceOnCompletedFlags.UseSchedulingContext); } else { @@ -176,7 +176,7 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b } else if (obj != null) { - Unsafe.As>(obj).OnCompleted(ThreadPool.s_invokeAsyncTask, box, _value._token, ValueTaskSourceOnCompletedFlags.UseSchedulingContext); + Unsafe.As>(obj).OnCompleted(ThreadPool.s_invokeAsyncStateMachineBox, box, _value._token, ValueTaskSourceOnCompletedFlags.UseSchedulingContext); } else { diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs index 2967083271243a..45fa46357aeb30 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs @@ -1415,16 +1415,26 @@ public static partial class ThreadPool internal static readonly ThreadPoolWorkQueue s_workQueue = new ThreadPoolWorkQueue(); - /// Shim used to invoke async tasks or non-task IAsyncStateMachineBoxes. - internal static readonly Action s_invokeAsyncTask = static state => + /// Shim used to invoke of the supplied . + internal static readonly Action s_invokeAsyncStateMachineBox = static state => { - if (state is Task t) + if (state is IAsyncStateMachineBox box) { - t.ExecuteDirectly(null); + box.MoveNext(); } - else if (state is IAsyncStateMachineBox box) + else { - box.MoveNext(); + ThrowHelper.ThrowUnexpectedStateForKnownCallback(state); + } + }; + + internal static readonly Action s_dispatchRuntimeAsyncContinuationsCallback = static state => + { + if (state is Task t) + { + // We know RuntimeAsyncTask overrides this and calls + // DispatchContinuations without looking at the Thread. + t.ExecuteFromThreadPool(null!); } else { @@ -1633,15 +1643,15 @@ public static bool UnsafeQueueUserWorkItem(Action callBack, TSta ThrowHelper.ThrowArgumentNullException(ExceptionArgument.callBack); } - // If the callback is the runtime-provided invocation of an async task (AsyncStateMachineBox, RuntimeAsyncTask) - // or non-task IAsyncStateMachineBox (PoolingAsyncValueTaskMethodBuilder.StateMachineBox), then we can queue - // its state directly to the ThreadPool instead of wrapping it in a QueueUserWorkItemCallback. + // If the callback is the runtime-provided invocation of an IAsyncStateMachineBox, + // then we can queue the Task state directly to the ThreadPool instead of + // wrapping it in a QueueUserWorkItemCallback. // // This occurs when user code queues its provided continuation to the ThreadPool; - // internally we call UnsafeQueueUserWorkItemInternal directly for these cases, whenever possible. - if (ReferenceEquals(callBack, s_invokeAsyncTask)) + // internally we call UnsafeQueueUserWorkItemInternal directly for Tasks. + if (ReferenceEquals(callBack, s_invokeAsyncStateMachineBox)) { - if (state is not Task && state is not IAsyncStateMachineBox) + if (state is not IAsyncStateMachineBox) { // The provided state must be the internal IAsyncStateMachineBox (Task) type ThrowHelper.ThrowUnexpectedStateForKnownCallback(state); @@ -1651,6 +1661,20 @@ public static bool UnsafeQueueUserWorkItem(Action callBack, TSta return true; } + // Similarly, for runtime async, user code may call with the + // runtime async callback directly. + if (ReferenceEquals(callBack, s_dispatchRuntimeAsyncContinuationsCallback)) + { + if (state is not Task) + { + // The provided state must be the internal RuntimeAsyncTask (Task) + ThrowHelper.ThrowUnexpectedStateForKnownCallback(state); + } + + UnsafeQueueUserWorkItemInternal((object)state, preferLocal); + return true; + } + s_workQueue.Enqueue( new QueueUserWorkItemCallbackDefaultContext(callBack, state), forceGlobal: !preferLocal); From eb3895d1c25f5c0e2ca7bb6f1ace393f400f15ff Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Mon, 11 May 2026 14:05:45 +0200 Subject: [PATCH 6/6] Clean up --- .../System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs | 2 -- .../src/System/Threading/Tasks/Task.cs | 2 +- .../src/System/Threading/ThreadPoolWorkQueue.cs | 5 ++--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs index d8a3a27188005e..da990797eb246a 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs @@ -374,8 +374,6 @@ public RuntimeAsyncTask() m_stateFlags |= (int)InternalTaskOptions.HiddenState; } - // Note that ThreadPool.s_dispatchRuntimeAsyncContinuationsCallback - // calls this function and always passes null for the thread. internal override void ExecuteDirectly(Thread? threadPoolThread) { DispatchContinuations(); diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs index b486dc171a8764..841c29ea5b96b0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @@ -2424,7 +2424,7 @@ internal bool ExecuteEntry() /// /// This is used internally to execute the Task directly. ThreadPool uses this, - /// and it is also used to invoke async state machines and runtime async tasks directly. + /// and it is also used to invoke runtime async tasks directly. /// The base behavior is simply to use the entry point that's not protected from /// double-invoke; derived internal tasks can override to customize their behavior, /// which is usually done by promises that want to reuse the same object as a queued work item. diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs index 45fa46357aeb30..35e49dce8d7d06 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs @@ -1428,13 +1428,12 @@ public static partial class ThreadPool } }; + /// Shim used to invoke of a supplied . internal static readonly Action s_dispatchRuntimeAsyncContinuationsCallback = static state => { if (state is Task t) { - // We know RuntimeAsyncTask overrides this and calls - // DispatchContinuations without looking at the Thread. - t.ExecuteFromThreadPool(null!); + t.ExecuteDirectly(null); } else {