-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Make runtime async callable thunks transparent #120386
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make runtime async callable thunks transparent #120386
Conversation
Runtime async callable thunks were using `TaskAwaiter` directly, but that has the normal await semantics which will either post continuations to the captured synchronization context or to the thread pool. This introduces an observable behavior change with async1 where sometimes even configured awaits will end up posting back to a captured synchronization context. For example, consider an example like: ```csharp private static async Task Foo() { SynchronizationContext.SetSynchronizationContext(new TrackingSynchronizationContext()); await Task.Delay(1000).ConfigureAwait(false); } ```csharp Before this change the runtime async call to `Task.Delay` creates a runtime async callback thunk that roughly looks like ```csharp Task DelayThunk(int time) { TaskAwaiter awaiter = Task.Delay(time).GetAwaiter(); if (!await.IsCompleted) AsyncHelpers.UnsafeAwaiterAwaiter(awaiter); awaiter.GetResult(); } ``` however, when this thunk is called we end up posting back to `TrackingSynchronizationContext`, before the continuation for `Foo` then must move its continuation back to the thread pool. This PR fixes this and makes the thunks transparent in context behavior. At the same time it also optimizes the runtime async -> async1 path to be more efficient in the suspension case: the async1 task now directly invokes the runtime async infrastructure as its continuion, instead of going through multiple layers of indirection.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs
Outdated
Show resolved
Hide resolved
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs
Show resolved
Hide resolved
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs
Outdated
Show resolved
Hide resolved
I think this approach will work. I'd like to see ValueTask support. There could be some trickiness there because of I think we might want to special case "IsCompleted" scenario for valuetask - to not make a Task when none is needed, but the rest can be done via
The most common case is |
We can definitely do it via |
I implemented the ValueTask part now @VSadov. Do you mind testing this on your libraries tests PR? |
I see the results as expected in the Libraries PR + these changes. Nearly everything passes except timeouts in IO.Pipelines, which is a separate issue. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
…time-async-callable-thunks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR fixes runtime async callable thunks to be transparent with respect to synchronization context behavior, addressing an issue where async1 methods using ConfigureAwait(false)
would still post continuations back to captured synchronization contexts.
Key changes include:
- Replacing
TaskAwaiter
with direct Task operations to avoid capturing synchronization context - Optimizing the runtime async to async1 path for better suspension handling
- Fixing a bug with
INotifyCompletion
vsICriticalNotifyCompletion
variant selection
Reviewed Changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
File | Description |
---|---|
src/tests/async/valuetask/valuetask.cs | Adds tests for runtime async callable thunks with ValueTask |
src/tests/async/synchronization-context/synchronization-context.cs | Adds test to verify no sync context capture in runtime callable thunks |
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs | Adds TryAddCompletionAction method for optimized continuation handling |
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs | Updates to use CriticalNotifier instead of Notifier for proper handling |
src/coreclr/vm/method.hpp | Removes unused method declaration |
src/coreclr/vm/metasig.h | Simplifies metasig definitions by removing awaiter-specific signatures |
src/coreclr/vm/corelib.h | Updates method definitions to use direct Task/ValueTask operations instead of awaiters |
src/coreclr/vm/asyncthunks.cpp | Major refactoring to generate IL that uses transparent Task operations |
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs | Renames ThunkTask to RuntimeAsyncTask and implements direct task completion handling |
Runtime async callable thunks were using
TaskAwaiter
directly, but that has the normal await semantics which will either post continuations to the captured synchronization context or to the thread pool. This introduces an observable behavior change with async1 where sometimes even configured awaits will end up posting back to a captured synchronization context.For example, consider an example like:
Before this change the runtime async call to
Task.Delay
creates a runtime async callback thunk that roughly looks likehowever, when this thunk is called we end up capturing the
TrackingSynchronizationContext
inside the thunk. Eventually that results in theDelayThunk
continuation being posted back to that synchronization context, followed byFoo
's continuation being moved back to the thread pool by the runtime async infrastructure.This PR fixes this and makes the thunks transparent in context behavior. At the same time it also optimizes the runtime async -> async1 path to be more efficient in the suspension case: the async1 task now directly invokes the runtime async infrastructure as its continuation, instead of going through multiple layers of indirection.
I also fixed a bug where we could end up invoking the wrong variant of
OnCompleted
for non-unsafe notifiers if they implemented bothINotifyCompletion
andICriticalNotifyCompletion
.I have also renamed
ThunkTask -> RuntimeAsyncTask
.Fix #119621 (likely...)