Skip to content

Save and restore Thread.CurrentThread._synchronizationContext for synchronous runtime async calls #117725

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

Open
wants to merge 30 commits into
base: main
Choose a base branch
from

Conversation

jakobbotsch
Copy link
Member

@jakobbotsch jakobbotsch commented Jul 16, 2025

This implements the remaining handling: saving and restoring Thread.CurrentThread._synchronizationContext around runtime async calls that finish synchronously. Lots of trickiness in representing this in the JIT. We introduce a new "suspended indicator" local that is defined by async calls and that the front end uses when it expands IR that restores the field.

Plenty of optimization opportunities remaining, including a simple jump threading optimization we should be able to do to make the suspension indicator variable disappear in common cases (all cases except when resuming with an exception).

Note: it might be beneficial to read my comment below to get a better understanding on what the IR being expanded by the JIT corresponds to.

@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 Jul 16, 2025
Copy link
Contributor

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

@jakobbotsch jakobbotsch reopened this Jul 18, 2025
@jakobbotsch jakobbotsch marked this pull request as ready for review July 18, 2025 12:55
@jakobbotsch
Copy link
Member Author

jakobbotsch commented Jul 18, 2025

cc @dotnet/jit-contrib PTAL @AndyAyersMS

This implements my last known piece of JIT-side compatibility/correctness for runtime async. With this I believe we should be fully compatible with async 1, but of course many pieces of optimizations are missing (and also there may be bugs).
I have run Fuzzlyn over this for many hours so it is tested for asserts to some extent, but Fuzzlyn's correctness coverage of SynchronizationContext and TaskScheduler is not good, so the actual compatibility with async1 is not as well tested. The should match the mental model I have built of SynchronizationContext over the past weeks/months. Maybe in the next few weeks I can introduce better SynchronizationContext testing to Fuzzlyn so I can increase the confidence in the compatibility.

cc @VSadov @agocke too. @agocke this will probably regress your benchmark somewhat due to the added ceremony around SynchronizationContext. Some of that can be optimized out in the future (e.g. see my comments above), while some will probably not be realistic/possible to remove.

@jakobbotsch jakobbotsch requested a review from AndyAyersMS July 18, 2025 13:03
Copy link
Member

@AndyAyersMS AndyAyersMS left a comment

Choose a reason for hiding this comment

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

LGTM. Left a few notes that can be handled in a follow-up.

@jakobbotsch
Copy link
Member Author

/ba-g Infra timeout, succeeded earlier

@jakobbotsch
Copy link
Member Author

@VSadov Any other feedback here?

const AsyncCallInfo& callInfo = call->GetAsyncInfo();
assert(callInfo.SaveAndRestoreSynchronizationContextField &&
(callInfo.SynchronizationContextLclNum != BAD_VAR_NUM));

Copy link
Member

@VSadov VSadov Jul 22, 2025

Choose a reason for hiding this comment

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

Is it also a right observation that either ExecContextGCDataIndex or ContinuationContextGCDataIndex present, not both?

Since the sync context is only captured when awaiting Tasks and exec context in Task case is captured into a local (not captured into continuation) - thus we do not need to think about a scenario when both continuation indices are set.

Copy link
Member

Choose a reason for hiding this comment

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

The structure of the code seems to allow that both contexts may be present in the continuation, but then it would raise questions on capturing exec context twice (before and after await), but I think it is not a real scenario.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe add an assert or a comment - so that whoever reads this does not need to worry about a combination that cannot happen.

Copy link
Member Author

Choose a reason for hiding this comment

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

Is it also a right observation that either ExecContextGCDataIndex or ContinuationContextGCDataIndex present, not both?

Yes, that's correct.

The structure of the code seems to allow that both contexts may be present in the continuation, but then it would raise questions on capturing exec context twice (before and after await), but I think it is not a real scenario.

CaptureContinuationContext is orthogonal here and has nothing to do with ExecutionContext. It only deals with SynchronizationContext and TaskScheduler. So even if both were set we would not be capturing ExecutionContext twice.

We capture ExecutionContext into a local if ExecutionContextHandling == SaveAndRestore and we capture it into ExecContextGCDataIndex if ExecutionContextHandling == AsyncSaveAndRestore. Former case is used for task awaits and latter for custom awaitables. So for natural reasons we will never do both.

The breakdown of the handling is the following:

  • For custom awaitables there is no special handling of SynchronizationContext or TaskScheduler. All the handling that exists is custom implemented by the user.
  • For custom awaitables there is special handling of ExecutionContext: when the custom awaitable suspends, the runtime/BCL ensure that the ExecutionContext will be captured and restored when the continuation is running.
  • For task awaits there is special handling of SynchronizationContext and TaskScheduler in multiple ways:
    • The runtime/BCL ensure that Thread.CurrentThread._synchronizationContext is saved and restored around synchronously finishing calls
    • The runtime/BCL ensure that when the callee suspends, the caller will eventually be resumed on the SynchronizationContext/TaskScheduler present before the call started. This resumption can be inlined if the SynchronizationContext is current when the continuation is about to run, and otherwise will be posted to it.
      Restoration of Thread.CurrentThread._synchronizationContext in this case is left up to the custom implementation of the SynchronizationContext, it is not handled by the runtime/BCL.
  • For task awaits the runtime/BCL ensure that Thread.CurrentThread._executionContext is captured before the call and restored after it. This happens consistently regardless of whether the callee finishes synchronously or not.

Does that make sense? I can add this comment inside AsyncCallInfo.

Copy link
Member

Choose a reason for hiding this comment

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

This all matches my understanding.

I mostly wanted to confirm that a combination that would lead to both contexts stored to the continuation does not exist.

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants