Skip to content

feat(Async): RunSynchronouslyImmediate - #19804

Merged
T-Gro merged 27 commits into
dotnet:mainfrom
bartelink:run-synchronously-immediate
Aug 6, 2026
Merged

feat(Async): RunSynchronouslyImmediate#19804
T-Gro merged 27 commits into
dotnet:mainfrom
bartelink:run-synchronously-immediate

Conversation

@bartelink

@bartelinkbartelink commented May 25, 2026

Copy link
Copy Markdown
Contributor

Implements RunSynchronouslyImmediate per fsharp/fslang-suggestions#1042

See also fsharp/fslang-suggestions#1467

Heavily revises the xmldoc for RunSynchronously in order to convey the tradeoffs involved in selecting between the new and the old.

Updated RunSynchronously:
image

NEW RunSynchronouslyImmediate:
image

NOTE xmldocs are intended to convey a nuanced message as explained in detail in #19804 (comment):

  • RunSychronously is still fine and reasonable to use
  • RSI offers the following key benefit:
    • until the first suspension point, you're directly on the same call stack so:
      • a breakpoint pause in a debugger will show a clean and simple call stack as for any synchronous call
      • an exception will show a single exception trace without any nesting
  • aside: you never pay for a thread hop (slight perf benefit; potentially significant in tight loops, but you're way off the beaten path if you have strong expectations of performance from RunSynchronously)
  • aside: a stack trace for an exception from RSI has two less frames [with wierd names like AsyncResult.Commit and QueueAsyncAndWaitForResultSynchronously

Checklist

  • Test cases added
    • validate absence of thread hops
    • contrast with Async.RunSynchronously
  • xmldoc updated
    • mention RSI in RS
    • mention RS in RSI
    • mention fact SynchronizationContext is not honored in RSI
    • mention RS ensures you're on a threadpool thread
    • DONT mention RSI does not force you onto a threadpool thread - my assumption is that RS does that for esoteric reasons we don't need to bother users of RSI with?
  • Release notes entry updated

@github-actions

github-actionsBot commented May 25, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Change pathRelease notes pathDescription
`src/FSharp.Core`docs/release-notes/.FSharp.Core/11.0.100.md
`src/Compiler`docs/release-notes/.FSharp.Compiler.Service/11.0.100.md

@bartelink
bartelinkforce-pushed the run-synchronously-immediate branch 2 times, most recently from d6a0470 to 69e41a2CompareMay 25, 2026 22:07
@bartelink
bartelink marked this pull request as ready for review May 25, 2026 22:19
@bartelink
bartelink requested a review from a team as a code ownerMay 25, 2026 22:19
CopilotAI review requested due to automatic review settings May 25, 2026 22:19

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds Async.RunSynchronouslyImmediate to FSharp.Core to allow running an Async<'T> synchronously while always executing the initial step on the calling thread (aimed at improved diagnostics/stack traces in FSI/tests), with accompanying API surface updates, documentation, unit tests, and release notes.

Changes:

  • Add public API Async.RunSynchronouslyImmediate and wire it through Async primitives.
  • Add unit tests characterizing basic behavior and key differences vs Async.RunSynchronously.
  • Update FSharp.Core surface area baselines and release notes; extend XML docs for RunSynchronously/RunSynchronouslyImmediate.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncModule.fsAdds unit tests for RunSynchronouslyImmediate and contrasts with RunSynchronously.
tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bslRecords new public surface area entry for RunSynchronouslyImmediate.
tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bslRecords new public surface area entry for RunSynchronouslyImmediate.
tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bslRecords new public surface area entry for RunSynchronouslyImmediate.
tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bslRecords new public surface area entry for RunSynchronouslyImmediate.
src/FSharp.Core/async.fsiAdds XML docs for the new API and revises RunSynchronously docs to reference it.
src/FSharp.Core/async.fsImplements the new API by exposing an “immediate” synchronous runner and refactoring RunSynchronously internals.
docs/release-notes/.FSharp.Core/11.0.100.mdAdds release note entry for Async.RunSynchronouslyImmediate.
Comments suppressed due to low confidence (1)

tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncModule.fs:527

  • This test uses a raw Thread but doesn’t capture exceptions from the thread body. Any unexpected exception inside the thread (including from Async.RunSynchronously) will be unhandled and may crash the test process rather than reporting a normal xUnit failure. Capture exceptions in the thread and rethrow/assert after Join.
 let t = Thread(fun () ->
callerThreadId <- Thread.CurrentThread.ManagedThreadId
async { runSyncThreadId <- Thread.CurrentThread.ManagedThreadId }
|> Async.RunSynchronously
async { immThreadId <- Thread.CurrentThread.ManagedThreadId }
|> Async.RunSynchronouslyImmediate)

Comment threadsrc/FSharp.Core/async.fsi Outdated
Comment threadsrc/FSharp.Core/async.fsi Outdated
Comment threadsrc/FSharp.Core/async.fsi Outdated
@github-actionsgithub-actionsBot added the AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files label May 25, 2026
Comment threadsrc/FSharp.Core/async.fsi Outdated
Comment threadsrc/FSharp.Core/async.fsi Outdated
@bartelink

bartelink commented May 26, 2026

Copy link
Copy Markdown
ContributorAuthor

@T-Gro I've implemented as per OP of fsharp/fslang-suggestions#1042

Firstly, in a debugger context a first chance exception breakpoint has more direct causation vs having to disentangle an adjacent thread waiting, which tooling may or may not be able to convey unaided.

However, while the exception stack trace has less noise, it's not significantly better AFAICT:

image

It seems the TL;DR value prop is:

  • in a debugger breakpoint there's an obvious call stack
  • stack traces have 2/3 less noise layers
  • default perf is better as no egregious thread hops (though for many real world cases, there may be a root RunSynchronously that pays the hop price and then nested calls don't pay?)

But its not a slam dunk as:

  • potential deadlocks
  • confusion about a longstanding thing that people's fingers and thousands of tutorials had

This is making me think I should dial back the xmldoc from where I have it trying to set a "use the new RunSynchronouslyImmediate in FSI from now on" vibe (as I've tried to do with AwaitTask vs Await where it is a slam dunk)

Final stacktrace comparison:
image

cc @majocha@dsyme

Comment threadsrc/FSharp.Core/async.fs Outdated
@bartelink
bartelinkforce-pushed the run-synchronously-immediate branch from 75fed30 to f9e0383CompareMay 26, 2026 15:40
Comment threadsrc/FSharp.Core/async.fs
@bartelink
bartelink requested review from T-Gro and CopilotMay 26, 2026 21:31

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 9 comments.

Comment threadsrc/FSharp.Core/async.fsi Outdated
Comment threadsrc/FSharp.Core/async.fsi Outdated
Comment threadsrc/FSharp.Core/async.fs
Comment threadsrc/FSharp.Core/async.fs Outdated
Comment threadsrc/FSharp.Core/async.fs
Comment threadsrc/FSharp.Core/async.fs
Comment threadsrc/FSharp.Core/async.fs
@bartelink

Copy link
Copy Markdown
ContributorAuthor

The open review comments and Don's comment here fsharp/fslang-suggestions#1042 (comment) make me think:

  1. implementing RunSynchronouslyBackground (fka RunImmediateExceptOnUI) might make it easier to explain the overall set of APIs, i.e.

    RSI guarantees inline running (but also potentially introduces synccontext deadlock)
    RSB = RSI + offloads iff necessary to avoid synccontext deadlock

    But, there's only one consumer of RSB atm, so what is the cut and dried use case. i.e. what would the summary/release notes say to convey who needs this API and when?

  2. if you had RSI+RSB and leave RS as-is (changing its behavior or guarantees is presumably not tenable), what new APIs with individual orthogonal behaviors would you add with a view to having better alternatives and then effectively deprecating RS?

    • Perhaps a RunSynchronouslyTimeout with a mandatory timeout parameter?
      • runs in threadpool unconditionally
      • provides some clear semantics that resolves the puzzle of what is being achieved / silently left unhandled in
        lettimeout,cancellationToken =
        match cancellationToken with
        | None -> timeout, defaultCancellationTokenSource.Token
        | Some token whennot token.CanBeCanceled -> timeout, token
        | Some token -> None, token
        +
        match res with
        | None ->// timed out
        // issue cancellation signal
        if innerCTS.IsSome then
        innerCTS.Value.Cancel()
        // wait for computation to quiesce; drop result on the floor
        resultCell.TryWaitForResultSynchronously()|> ignore
        // dispose the CancellationTokenSource
        if innerCTS.IsSome then
        innerCTS.Value.Dispose()
        raise (TimeoutException())
        | Some res ->
        match innerCTS with
        | Some subSource -> subSource.Dispose()
        | None ->()
        res.Commit()
        +
        match SynchronizationContext.Current, Thread.CurrentThread.IsThreadPoolThread, timeout with
        |null,true, None -> RunImmediate cancellationToken computation
        |_-> QueueAsyncAndWaitForResultSynchronously cancellationToken computation timeout
      • should the API take an outer CT and/or defaults to DefaultCancellationToken?
      • what happens if the token is cancelable and you wanted a timeout?
      • can the default one not be cancelable

In general I think one PR that adds one API is a perfectly fine outcome for now, but if someone out there (@dsyme ?) has a really clear picture of all the use cases, it may be worth at least speccing out in full the orthogonal elements of what only the current impl of RunSynchronously provides that are not covered by RSI+RSB

In short, if someone is convinced that RSI + RSB and/or some complementary APIs makes it possible to deprecate and eventually move away from from a complected RunSynchronously, I'm happy to do the work, but I don't feel I understand it deeply enough atm.

@majocha

Copy link
Copy Markdown
Contributor

FWIW my limited experience with internal RunImmediate implemented in a few places in this repo: It can cause problems in unexpected places, IIRC there were some deadlocks when trying to parallelize tests. As noted, it only improves debugger call stacks. Exception stack traces - not that much.

Comment threadsrc/FSharp.Core/async.fs
@T-GroT-Gro added the AI-reviewed PR reviewed by AI review council label May 27, 2026
@T-Gro

T-Gro commented Jun 4, 2026

Copy link
Copy Markdown
Member

@bartelink :
re: RunSynchronously - Deprecating it now would be I guess too early, but maybe we can at least start with a "See also .." remark in the XML docs (which then make it to the published docs).

And consider marking it obsolete later in a separate PR.

What RunSynchronously provides is the automated decision-making based on inputs+context. Even though it can be handy when writing library code (that does not know where/how it will be run), it does go against the "Pit of success" spirit.

@bartelink

Copy link
Copy Markdown
ContributorAuthor

@T-Gro I believe this is ready for re-review; feel free to mark comment threads as resolved as you read them.

Happy to address any follow-ups later and/or tomorrow

@T-GroT-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Green CI and the review is essentially done — ready to approve once the last small items land:

  • shim guard (Common.fs, Utilities.fs): drop the #if and always include the shim until an 11.x FSharp.Core ships the member (per the thread — the constant is never defined for the test projects, so the guard never toggled).
  • async.fsi:50 RS summary: drop "honoring the ambient SynchronizationContext" per the thread.

Ping me when those are in and I'll approve.

@T-Gro

Copy link
Copy Markdown
Member

Green CI and the review is essentially done — ready to approve once the last small items land:

  • shim guard (Common.fs, Utilities.fs): drop the #if and always include the shim until an 11.x FSharp.Core ships the member (per the thread — the constant is never defined for the test projects, so the guard never toggled).
  • async.fsi:50 RS summary: drop "honoring the ambient SynchronizationContext" per the thread.

Ping me when those are in and I'll approve.

@bartelink

bartelink commented Jul 27, 2026

Copy link
Copy Markdown
ContributorAuthor

Ping me when those are in and I'll approve.

@T-Gro I believe this is ready for final review; the above two points have been addressed (see #19804 (comment))

@T-Gro

T-Gro commented Aug 4, 2026

Copy link
Copy Markdown
Member

🤖🕵️
@bartelinkRe the failing Windows jobs: the desktop (#else) branch of a test wasn't updated to the new name this PR introduces — tests/FSharp.Compiler.Service.Tests/ErrorList/ScriptDiagnosticsTests.fs:23 still calls Async.RunImmediate, giving error FS0039: … 'RunImmediate'. Update that one call site to Async.RunSynchronouslyImmediate. (No FSharp.Core change — just the test catching up.)

@bartelink

Copy link
Copy Markdown
ContributorAuthor

@T-Gro will merge latest, adjust any straggler RunImmediates and @ you later when I get a chance

@bartelink

Copy link
Copy Markdown
ContributorAuthor

@T-Gro looks like this has finally made it ;)

@github-project-automationgithub-project-automationBot moved this from New to In Progress in F# Compiler and ToolingAug 6, 2026
@T-Gro
T-Gro merged commit 9357a4f into dotnet:mainAug 6, 2026
48 checks passed
@github-project-automationgithub-project-automationBot moved this from In Progress to Done in F# Compiler and ToolingAug 6, 2026
@T-Gro

T-Gro commented Aug 6, 2026

Copy link
Copy Markdown
Member

@bartelink : Thank you for your persistence and congratulations 👍

@bartelink
bartelink deleted the run-synchronously-immediate branch August 6, 2026 13:03
bartelink added a commit to bartelink/fsharp that referenced this pull request Aug 6, 2026
bartelink added a commit to bartelink/fsharp that referenced this pull request Aug 11, 2026
- rename and sync clone impls withing VisualFSharp.slnx as per previous PR
- update RunImmediateExceptOnUI to delegate and follow naming
bartelink added a commit to bartelink/fsharp that referenced this pull request Aug 11, 2026
- rename and sync clone impls withing VisualFSharp.slnx as per previous PR
- update RunImmediateExceptOnUI to delegate and follow naming
T-Gro pushed a commit that referenced this pull request Aug 12, 2026
* chore(Async.RunSynchronouslyImmediate): Stragglers from #19804
- rename and sync clone impls withing VisualFSharp.slnx as per previous PR
- update RunImmediateExceptOnUI to delegate and follow naming
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI-reviewedPR reviewed by AI review councilAI-Tooling-Check-Scanned-CleanTooling check: diff analyzed, no interesting infrastructure files

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

4 participants

@bartelink@majocha@T-Gro