Skip to content

Conversation

@jakobbotsch
Copy link
Member

@jakobbotsch jakobbotsch commented Nov 7, 2025

This fixes an issue where there was a behavioral difference between async1 and async2 for async to sync runtime async calls. Before this PR the JIT would always save and restore contexts around awaits of task-returning methods, regardless of whether they were implemented as runtime async functions or not. That does not match async1: in async1, the context save and restore happens around the body in each async method. The former logic was an optimization, but the optimization is only correct for runtime async to runtime async calls.

We cannot in general know if a callee is implemented by runtime async or not, so we have to move the context save/restore to happen around the body of all runtime async methods.

An example test program that saw the behavioral difference before is the following:

using System;
using System.Threading;
using System.Threading.Tasks;

public class Program
{
    static void Main()
    {
        Foo().GetAwaiter().GetResult();
    }

    static async Task Foo()
    {
        SynchronizationContext.SetSynchronizationContext(new MySyncCtx(123));
        Console.WriteLine(((MySyncCtx)SynchronizationContext.Current).Arg);
        await Bar();
        Console.WriteLine(((MySyncCtx)SynchronizationContext.Current).Arg);
    }

    static Task Bar()
    {
        SynchronizationContext.SetSynchronizationContext(new MySyncCtx(124));
        return Task.CompletedTask;
    }

    class MySyncCtx(int arg) : SynchronizationContext
    {
        public int Arg => arg;
    }
}

Async1: 123 124
Runtime async before: 123 123
Runtime async after: 123 124

@github-actions github-actions bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Nov 7, 2025
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@jakobbotsch
Copy link
Member Author

FYI @VSadov @davidwrighton

Still not ready -- needs more testing, and I need to introduce the test case. But this is a heads up that some changes are incoming around the save/restore behavior.

@davidwrighton
Copy link
Member

davidwrighton commented Nov 7, 2025

This doesn't feel right around Sync/Execution save and restore at suspension points. It looks correct for save/restore around the initial call, but the async callsite on suspension behavior looks wrong to me. Let me see if I can write up a few test cases. Do we have a convenient async testbed for these sorts of tests? In general, I think save/restore at the top level makes sense, but using that context saved at the top level during a suspend as the "captured" context I think is wrong.

@jakobbotsch
Copy link
Member Author

jakobbotsch commented Nov 7, 2025

Let me see if I can write up a few test cases. Do we have a convenient async testbed for these sorts of tests?

src/tests/async is where they live, there is a synchronization context test.

using that context saved at the top level during a suspend as the "captured" context I think is wrong.

We aren't doing that, we are getting new contexts from Thread.CurrentThread for saving inside the continuation (and for restoring on resumption). Then we restore the thread ones from the ones we saved at the beginning.
(This path is currently pretty inefficient, we could have a single helper to save the current contexts and restore the old ones, but I left that for a future PR)

@davidwrighton
Copy link
Member

@jakobbotsch Ah, I was confused by some of the terminology around the JIT here. What happens if there is an exception while restoring the old contexts on suspension? Are we still inside the try/finally which will ALSO attempt to restore the old contexts? Notably, restoring a context involves running arbitrary code since you can have an AsyncLocal with value changed notifications, and what is the behavior if they throw? I don't want us to try to run the ExecutionContext restore twice. I have no idea what that would do. Now that I've looked at this, I think otherwise its all good.

@jakobbotsch
Copy link
Member Author

What happens if there is an exception while restoring the old contexts on suspension? Are we still inside the try/finally which will ALSO attempt to restore the old contexts? Notably, restoring a context involves running arbitrary code since you can have an AsyncLocal with value changed notifications, and what is the behavior if they throw? I don't want us to try to run the ExecutionContext restore twice. I have no idea what that would do. Now that I've looked at this, I think otherwise its all good.

No, suspension/resumption code is not inside the try clause. So if OnValuesChanges throws we won't retry the restore.
Still, it is a good point about exceptions during suspension. I opened #121465 to investigate that further.

@VSadov
Copy link
Member

VSadov commented Nov 10, 2025

Do we still need context store/restore in task-returning thunks after this?

CC: @eduardo-vp

@jakobbotsch
Copy link
Member Author

Do we still need context store/restore in task-returning thunks after this?

CC: @eduardo-vp

No, I do not think it is needed. We can switch it to just ensure Thread.CurrentThread is initialized. I think I will leave it as a follow up.

@jakobbotsch
Copy link
Member Author

Anything else here @AndyAyersMS?

Copy link
Member

@VSadov VSadov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks!

@jakobbotsch
Copy link
Member Author

/ba-g Timeouts

@jakobbotsch jakobbotsch merged commit 3baf6d2 into dotnet:main Nov 13, 2025
100 of 107 checks passed
@jakobbotsch jakobbotsch deleted the save-restore-around-body branch November 13, 2025 12:34
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Nov 14, 2025
This was added in dotnet#121448 on the VM side, managed side needs a matching change (I actually found this because we had a newly failing test, yay).
jkotas pushed a commit that referenced this pull request Nov 14, 2025
This was added in #121448 on the VM side, managed side needs a matching
change (I actually found this because we had a newly failing test, yay).

Cc @dotnet/ilc-contrib @jakobbotsch
jakobbotsch added a commit to jakobbotsch/runtime that referenced this pull request Nov 18, 2025
With dotnet#121448 we are saving/restoring the contexts as we unwind during
suspension. However, since we delay the clal to OnCompleted, we need to
restore the leaf contexts explicitly now.
VSadov pushed a commit to VSadov/runtime that referenced this pull request Nov 18, 2025
With dotnet#121448 we are saving/restoring the contexts as we unwind during
suspension. However, since we delay the clal to OnCompleted, we need to
restore the leaf contexts explicitly now.
VSadov pushed a commit to VSadov/runtime that referenced this pull request Nov 18, 2025
With dotnet#121448 we are saving/restoring the contexts as we unwind during
suspension. However, since we delay the clal to OnCompleted, we need to
restore the leaf contexts explicitly now.
VSadov pushed a commit to VSadov/runtime that referenced this pull request Nov 19, 2025
With dotnet#121448 we are saving/restoring the contexts as we unwind during
suspension. However, since we delay the clal to OnCompleted, we need to
restore the leaf contexts explicitly now.
VSadov pushed a commit to VSadov/runtime that referenced this pull request Nov 19, 2025
With dotnet#121448 we are saving/restoring the contexts as we unwind during
suspension. However, since we delay the clal to OnCompleted, we need to
restore the leaf contexts explicitly now.
jakobbotsch added a commit that referenced this pull request Nov 23, 2025
With #121448 we are restoring contexts as we unwind during suspension.
However, since we delay the call to `OnCompleted`, we need to restore
the leaf contexts explicitly now.

Also change suspension so that we do not notify about context restores. This is
different from async1, but it is necessary to keep calling `OnCompleted` in the
right contexts when we get back to `RuntimeAsyncTask`. Also, skipping
notifications for contexts when we know no user code will be executed is one of
the optimizations we want to allow with runtime async.
@github-actions github-actions bot locked and limited conversation to collaborators Dec 14, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI runtime-async

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants