-
Notifications
You must be signed in to change notification settings - Fork 869
feat(Async): RunSynchronouslyImmediate #19804
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
base: main
Are you sure you want to change the base?
Changes from all commits
42e2a8d
5240c49
0e6b507
a085f84
ef47597
89575ee
8cbbbb4
5074cab
4d26acf
2b1e1fc
ea790f5
ca1b254
d1c658a
dff9ed1
f7cd0e0
0a7eb30
70bd071
172d6cd
8422a23
3393014
ed96e84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 = | ||
| 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>) = | ||
|
|
@@ -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 = | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
@@ -1138,31 +1154,24 @@ 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)) | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. old behavior resulting in OperationCanceledException
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does it change RunSynchronously implementation?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @majocha in the PR as it stands, the core impl of RunSynchronouslyImmediate is:
The key diff is that:
The pros from my perspective are:
Not an easy call, which is why I'm looking for someone else to vouch for it! |
||
| 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) | ||
| tcs.SetException | ||
| // 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 | ||
|
bartelink marked this conversation as resolved.
|
||
|
|
||
| [<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 | ||
|
bartelink marked this conversation as resolved.
|
||
| | _ -> QueueAsyncAndWaitForResultSynchronously computation cancellationToken timeout // less useful stack traces | ||
|
|
||
| [<DebuggerHidden>] | ||
| let Start cancellationToken (computation: Async<unit>) = | ||
|
|
@@ -1174,22 +1183,6 @@ module AsyncPrimitives = | |
| computation | ||
| |> unfake | ||
|
|
||
| [<DebuggerHidden>] | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
@@ -1511,7 +1504,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 = | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SynchronizationContext.SetSynchronizationContext ctx
async { return 1 } |> Async.RunSynchronously // ctx.Post called 0 times; runs inline
- /// <summary><p>Runs the asynchronous computation on a threadpool thread, honoring the ambient
- /// <see cref="T:System.Threading.SynchronizationContext"/>.</p>
+ /// <summary><p>Runs the computation and blocks the caller until it completes. Runs inline on the
+ /// calling thread when it is a thread-pool thread with no ambient SynchronizationContext and no
+ /// timeout; otherwise runs on the thread pool.</p>
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Your wording feels much better (I was aware that my wording was technically not 100% correct - my aim was to emphasize the core difference vs RSI: even if you are doing something tiny, you'll incur a thread hop more often than you might expect from the name). Also wanted to be pedantic on some specifics:
but fast path requires you to already be on a thread pool thread. So RunSynchronously didnt need to do a hop, but the code is run on a thread pool thread?
I think this is due to special casing triggered by the But I may well have missed the point you're trying to make. Happy to apply your summary assuming all the above tallies with your understanding/intent. |
||
| /// <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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. async { return 1 } |> Async.RunSynchronously // also runs inline -- not a threadpool guarantee
- /// <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>
+ /// <p>Does not ensure execution takes place on a threadpool thread; see
+ /// <see cref="M:Microsoft.FSharp.Control.FSharpAsync.SwitchToThreadPool"/> if this is required.</p>
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The key driver for me referring to both was to try to explain the key behavioral difference of consequence between RS and RSI in terms of other concrete methods as a pedagogical mechanism. AIUI (but I may very well be wrong)
Note I renamed the internal impl to All of that said, clearly my wording didn't get the point across as intended. Hence I feel it may make more sense to reword it and/or remove it entirely as the proposed change turns it into a non-sequitur as the sentence no longer enables people to triangulate/bridge the concepts as intended? |
||
| /// <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> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.