Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/release-notes/.FSharp.Core/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@

* Fix `Array.exists2` documentation examples to use equal-length arrays; the previous examples would throw `ArgumentException` at runtime instead of returning the documented `false`/`true` values. ([PR #19672](https://github.com/dotnet/fsharp/pull/19672))
* Move `Async.StartChild` to the "Starting Async Computations" docs category alongside `Async.StartChildAsTask`. ([Issue #19667](https://github.com/dotnet/fsharp/issues/19667))

### Added

* Add `Async.RunSynchronouslyImmediate`, which runs the work until the first asynchronous suspension on the calling thread (as opposed to `RunSynchronously`, which immediately offloads if not on a background and/or threadpool thread). ([Issue #1042](https://github.com/fsharp/fslang-suggestions/issues/1042), [PR #19804](https://github.com/dotnet/fsharp/pull/19804))
79 changes: 40 additions & 39 deletions src/FSharp.Core/async.fs
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,22 @@ module AsyncPrimitives =
ccont = (fun cexn -> ctxt.PostWithTrampoline syncCtxt (fun () -> ctxt.ccont cexn))
)

[<DebuggerHidden>]
let StartWithContinuations cancellationToken (computation: Async<'T>) cont econt ccont =
Copy link
Copy Markdown
Author

@bartelink bartelink May 26, 2026

Choose a reason for hiding this comment

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

moved up as new RunSynchronouslyImmediate needs it (putting it there as the diffs look even more confusing if I put it as far down the file as possible)

let trampolineHolder = TrampolineHolder()

trampolineHolder.ExecuteWithTrampoline(fun () ->
let ctxt =
AsyncActivation.Create
cancellationToken
trampolineHolder
(cont >> fake)
(econt >> fake)
(ccont >> fake)

computation.Invoke ctxt)
|> unfake

[<Sealed>]
[<AutoSerializable(false)>]
type SuspendedAsync<'T>(ctxt: AsyncActivation<'T>) =
Expand Down Expand Up @@ -1096,7 +1112,7 @@ module AsyncPrimitives =

/// Run the asynchronous workflow and wait for its result.
[<DebuggerHidden>]
let QueueAsyncAndWaitForResultSynchronously (token: CancellationToken) computation timeout =
let QueueAsyncAndWaitForResultSynchronously computation (token: CancellationToken) timeout =
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

can revert this, but trying to get arg order consistent across most involved functions

let token, innerCTS =
// If timeout is provided, we govern the async by our own CTS, to cancel
// when execution times out. Otherwise, the user-supplied token governs the async.
Expand Down Expand Up @@ -1138,31 +1154,26 @@ module AsyncPrimitives =
res.Commit()

[<DebuggerHidden>]
let RunImmediate (cancellationToken: CancellationToken) computation =
use resultCell = new ResultCell<AsyncResult<_>>()
let trampolineHolder = TrampolineHolder()

trampolineHolder.ExecuteWithTrampoline(fun () ->
let ctxt =
AsyncActivation.Create
cancellationToken
trampolineHolder
(fun res -> resultCell.RegisterResult(AsyncResult.Ok res, reuseThread = true))
(fun edi -> resultCell.RegisterResult(AsyncResult.Error edi, reuseThread = true))
(fun exn -> resultCell.RegisterResult(AsyncResult.Canceled exn, reuseThread = true))
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

old behavior resulting in OperationCanceledException

let RunSynchronouslyImmediate<'T> computation (cancellationToken: CancellationToken) =
let tcs = TaskCompletionSource<'T>()

computation.Invoke ctxt)
|> unfake

let res = resultCell.TryWaitForResultSynchronously().Value
res.Commit()
StartWithContinuations
cancellationToken
computation
tcs.SetResult
(fun edi -> tcs.SetException edi.SourceException)
// NOTE In this case, cancellation will surface as a TaskCanceledException (with CT.None) from GetResult()
// (as opposed to the OperationCanceledException that RegisterResult cancellation ends up mapping to)
(fun _operationCanceledExn -> tcs.SetCanceled())
// Synchronously block waiting for the result (i.e. even if continuations run on another thread, caller thread will be blocked)
tcs.Task.GetAwaiter().GetResult() // GetResult() unpacks the AggregateException that .Result would present

[<DebuggerHidden>]
let RunSynchronously cancellationToken (computation: Async<'T>) timeout =
// Reuse the current ThreadPool thread if possible.
let RunSynchronouslyBackgroundThreadPool (computation: Async<'T>) cancellationToken timeout =
// Run inline only where it's guaranteed to be safe
match SynchronizationContext.Current, Thread.CurrentThread.IsThreadPoolThread, timeout with
| null, true, None -> RunImmediate cancellationToken computation
| _ -> QueueAsyncAndWaitForResultSynchronously cancellationToken computation timeout
| null, true, None -> RunSynchronouslyImmediate computation cancellationToken // best stacktrace in case of exception
Comment on lines +1157 to +1175
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@T-Gro this is the key implication of settling on a uniform simplified impl of RunSynchronouslyImmediate shared between it and [the no-thread-hop path of] RunSynchronously

IMO If we roll it back, we get messier stack traces and uglier code so I'm suggesting we take this inconsistency hit.

| _ -> QueueAsyncAndWaitForResultSynchronously computation cancellationToken timeout // less useful stack traces

[<DebuggerHidden>]
let Start cancellationToken (computation: Async<unit>) =
Expand All @@ -1174,22 +1185,6 @@ module AsyncPrimitives =
computation
|> unfake

[<DebuggerHidden>]
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

moved up as required by RunSynchronously

note the whole PR could be made much shorter by having the Async layer call Async.FromContinuations

However, as mentioned in other places, I believe its for the best that RunSynchronouslyImmediate and the immediate path of RunSychronously should have strong clear ties in the lower layer as this PR does

let StartWithContinuations cancellationToken (computation: Async<'T>) cont econt ccont =
let trampolineHolder = TrampolineHolder()

trampolineHolder.ExecuteWithTrampoline(fun () ->
let ctxt =
AsyncActivation.Create
cancellationToken
trampolineHolder
(cont >> fake)
(econt >> fake)
(ccont >> fake)

computation.Invoke ctxt)
|> unfake

[<DebuggerHidden>]
let StartAsTask cancellationToken (computation: Async<'T>) taskCreationOptions =
let taskCreationOptions = defaultArg taskCreationOptions TaskCreationOptions.None
Expand Down Expand Up @@ -1511,7 +1506,13 @@ type Async =
| Some token when not token.CanBeCanceled -> timeout, token
| Some token -> None, token

RunSynchronously cancellationToken computation timeout
RunSynchronouslyBackgroundThreadPool computation cancellationToken timeout

static member RunSynchronouslyImmediate(computation: Async<'T>, ?cancellationToken: CancellationToken) =
let cancellationToken =
defaultArg cancellationToken defaultCancellationTokenSource.Token

RunSynchronouslyImmediate computation cancellationToken

static member Start(computation, ?cancellationToken) =
let cancellationToken =
Expand Down
96 changes: 66 additions & 30 deletions src/FSharp.Core/async.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -47,50 +47,86 @@ namespace Microsoft.FSharp.Control
[<CompiledName("FSharpAsync")>]
type Async =

/// <summary>Runs the asynchronous computation and await its result.</summary>
///
/// <remarks>If an exception occurs in the asynchronous computation then an exception is re-raised by this
/// function.
///
/// If no cancellation token is provided then the default cancellation token is used.
///
/// The computation is started on the current thread if <see cref="P:System.Threading.SynchronizationContext.Current"/> is null,
/// <see cref="P:System.Threading.Thread.CurrentThread"/> has <see cref="P:System.Threading.Thread.IsThreadPoolThread"/>
/// of <c>true</c>, and no timeout is specified. Otherwise the computation is started by queueing a new work item in the thread pool,
/// and the current thread is blocked awaiting the completion of the computation.
///
/// The timeout parameter is given in milliseconds. A value of -1 is equivalent to
/// <see cref="F:System.Threading.Timeout.Infinite"/>.
/// <summary><p>Runs the asynchronous computation on a threadpool thread, honoring the ambient
/// <see cref="T:System.Threading.SynchronizationContext"/>.</p>
/// <p>During processing, the calling thread blocks awaiting the outcome.</p>
/// </summary>
/// <remarks>
/// <p>Note For F# interactive, F# scripts, and unit tests consider using
/// <see cref="M:Microsoft.FSharp.Control.FSharpAsync.RunSynchronouslyImmediate`1"/>, which
/// always starts on the calling thread and presents a simpler stack trace in exception cases and/or under a debugger.</p>
/// <p>Computation runs directly on the calling thread when
/// <see cref="P:System.Threading.SynchronizationContext.Current"/> is <c>null</c>,
/// <see cref="P:System.Threading.Thread.IsThreadPoolThread"/> is <c>true</c>, and no timeout is specified.</p>
/// </remarks>
///
/// <param name="computation">The computation to run.</param>
/// <param name="timeout">The amount of time in milliseconds to wait for the result of the
/// computation before raising a <see cref="T:System.TimeoutException"/>. If no value is provided
/// for timeout then a default of -1 is used to correspond to <see cref="F:System.Threading.Timeout.Infinite"/>.</param>
/// <param name="timeout">The number of milliseconds to wait for the result of the
/// computation before raising a <see cref="T:System.TimeoutException"/>. If no value or -1 is provided
/// the timeout will be <see cref="F:System.Threading.Timeout.Infinite"/>.</param>
/// <param name="cancellationToken">The cancellation token to be associated with the computation.
/// If one is not supplied, the default cancellation token is used.</param>
///
/// <returns>The result of the computation.</returns>
///
/// If omitted, <c>Async.DefaultCancellationToken</c> is used.</param>
/// <returns>The result of the computation. Any exception raised by the computation is propagated to the caller.</returns>
/// <category index="0">Starting Async Computations</category>
///
/// <example id="run-synchronously-1">
/// <code lang="fsharp">
/// printfn "A"
/// printfn "A" // runs on caller thread
///
/// let result = async {
/// printfn "B"
/// printfn "B" // runs on a background/threadpool thread
/// do! Async.Sleep(1000)
/// printfn "C"
/// 17
/// printfn "C" // continuation runs on a background/threadpool thread
/// return 17
/// } |> Async.RunSynchronously
///
/// printfn "D"
/// printfn "D" // runs on caller thread
/// </code>
/// Prints "A", "B" immediately, then "C", "D" in 1 second. result is set to 17.
/// <p>Prints "A", "B" immediately, then "C", "D" after 1 second.</p>
/// <p>Yields <c>result = 17</c>.</p>
/// </example>
static member RunSynchronously : computation:Async<'T> * ?timeout : int * ?cancellationToken:CancellationToken-> 'T


/// <summary><p>Starts the asynchronous computation on the calling thread, disregarding the ambient
/// <see cref="T:System.Threading.SynchronizationContext"/>.</p>
/// <p>During any asynchronous continuations after the first suspension, the calling thread blocks awaiting the outcome.</p>
/// </summary>
/// <remarks>
/// <p>Warning: blocks the calling thread for the duration of the computation. Calling it
/// from a UI thread will make the UI unresponsive and risks deadlock if any continuation in the
/// computation needs to be dispatched back to that context.</p>
/// <p>Normally preferred to <see cref="M:Microsoft.FSharp.Control.FSharpAsync.RunSynchronously`1"/> for
/// interactive use in F# scripts and F# interactive (FSI), and for unit tests as: <br/>
/// - a breakpoint will show a clearer call stack prior to the first suspension (as opposed to it waiting for an asynchronous completion notification from another thread<br/>
/// - the stack trace in the case of an exception will have two fewer frames.
/// </p>
/// <p>Does not support a timeout; see
/// <see cref="M:Microsoft.FSharp.Control.FSharpAsync.RunSynchronously`1"/> if one is desired.</p>
/// <p>Does not ensure execution takes place on a threadpool thread; see
/// <see cref="M:Microsoft.FSharp.Control.FSharpAsync.RunSynchronously`1"/> or
/// <see cref="M:Microsoft.FSharp.Control.FSharpAsync.SwitchToThreadPool"/> if this is required.</p>
/// </remarks>
/// <param name="computation">The computation to run.</param>
/// <param name="cancellationToken">The cancellation token to be associated with the computation.
/// If omitted, <c>Async.DefaultCancellationToken</c> is used.</param>
/// <returns>The result of the computation. Any exception raised by the computation is propagated to the caller.</returns>
/// <category index="0">Starting Async Computations</category>
/// <example id="run-synchronously-immediate-1">
/// <code lang="fsharp">
/// printfn "A" // runs on calling thread
///
/// let result = async {
/// printfn "B" // ALSO runs on calling thread (hence immediately)
/// do! Async.Sleep(1000)
/// printfn "C" // runs in continuation context (depends on SynchronizationContext etc)
/// return 17
/// } |> Async.RunSynchronouslyImmediate
///
/// printfn "D" // runs on calling thread
/// </code>
/// <p>Prints "A", "B" immediately, then "C", "D" after 1 second.</p>
/// <p>Yields <c>result = 17</c>.</p>
/// </example>
static member RunSynchronouslyImmediate : computation : Async<'T> * ?cancellationToken : CancellationToken -> 'T

/// <summary>Starts the asynchronous computation in the thread pool. Do not await its result.</summary>
///
/// <remarks>If no cancellation token is provided then the default cancellation token is used.</remarks>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,7 @@ Microsoft.FSharp.Control.FSharpAsync: System.Threading.Tasks.Task`1[T] StartAsTa
Microsoft.FSharp.Control.FSharpAsync: System.Threading.Tasks.Task`1[T] StartImmediateAsTask[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Microsoft.FSharp.Control.FSharpAsync: System.Tuple`3[Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`3[TArg,System.AsyncCallback,System.Object],System.IAsyncResult],Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T],Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,Microsoft.FSharp.Core.Unit]] AsBeginEnd[TArg,T](Microsoft.FSharp.Core.FSharpFunc`2[TArg,Microsoft.FSharp.Control.FSharpAsync`1[T]])
Microsoft.FSharp.Control.FSharpAsync: T RunSynchronously[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Int32], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Microsoft.FSharp.Control.FSharpAsync: T RunSynchronouslyImmediate[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Microsoft.FSharp.Control.FSharpAsync: Void CancelDefaultToken()
Microsoft.FSharp.Control.FSharpAsync: Void Start(Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Microsoft.FSharp.Control.FSharpAsync: Void StartImmediate(Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,7 @@ Microsoft.FSharp.Control.FSharpAsync: System.Threading.Tasks.Task`1[T] StartAsTa
Microsoft.FSharp.Control.FSharpAsync: System.Threading.Tasks.Task`1[T] StartImmediateAsTask[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Microsoft.FSharp.Control.FSharpAsync: System.Tuple`3[Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`3[TArg,System.AsyncCallback,System.Object],System.IAsyncResult],Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T],Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,Microsoft.FSharp.Core.Unit]] AsBeginEnd[TArg,T](Microsoft.FSharp.Core.FSharpFunc`2[TArg,Microsoft.FSharp.Control.FSharpAsync`1[T]])
Microsoft.FSharp.Control.FSharpAsync: T RunSynchronously[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Int32], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Microsoft.FSharp.Control.FSharpAsync: T RunSynchronouslyImmediate[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Microsoft.FSharp.Control.FSharpAsync: Void CancelDefaultToken()
Microsoft.FSharp.Control.FSharpAsync: Void Start(Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Microsoft.FSharp.Control.FSharpAsync: Void StartImmediate(Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,7 @@ Microsoft.FSharp.Control.FSharpAsync: System.Threading.Tasks.Task`1[T] StartAsTa
Microsoft.FSharp.Control.FSharpAsync: System.Threading.Tasks.Task`1[T] StartImmediateAsTask[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Microsoft.FSharp.Control.FSharpAsync: System.Tuple`3[Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`3[TArg,System.AsyncCallback,System.Object],System.IAsyncResult],Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T],Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,Microsoft.FSharp.Core.Unit]] AsBeginEnd[TArg,T](Microsoft.FSharp.Core.FSharpFunc`2[TArg,Microsoft.FSharp.Control.FSharpAsync`1[T]])
Microsoft.FSharp.Control.FSharpAsync: T RunSynchronously[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Int32], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Microsoft.FSharp.Control.FSharpAsync: T RunSynchronouslyImmediate[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Microsoft.FSharp.Control.FSharpAsync: Void CancelDefaultToken()
Microsoft.FSharp.Control.FSharpAsync: Void Start(Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Microsoft.FSharp.Control.FSharpAsync: Void StartImmediate(Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,7 @@ Microsoft.FSharp.Control.FSharpAsync: System.Threading.Tasks.Task`1[T] StartAsTa
Microsoft.FSharp.Control.FSharpAsync: System.Threading.Tasks.Task`1[T] StartImmediateAsTask[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Microsoft.FSharp.Control.FSharpAsync: System.Tuple`3[Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`3[TArg,System.AsyncCallback,System.Object],System.IAsyncResult],Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T],Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,Microsoft.FSharp.Core.Unit]] AsBeginEnd[TArg,T](Microsoft.FSharp.Core.FSharpFunc`2[TArg,Microsoft.FSharp.Control.FSharpAsync`1[T]])
Microsoft.FSharp.Control.FSharpAsync: T RunSynchronously[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Int32], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Microsoft.FSharp.Control.FSharpAsync: T RunSynchronouslyImmediate[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Microsoft.FSharp.Control.FSharpAsync: Void CancelDefaultToken()
Microsoft.FSharp.Control.FSharpAsync: Void Start(Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Microsoft.FSharp.Control.FSharpAsync: Void StartImmediate(Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken])
Expand Down
Loading
Loading