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.Await`, which mirrors `Async.AwaitTask` semantics, but elides egregious `AggregateException` wrapping. Includes ValueTask support, and a SRTP-based overload accepting any task-like value that supports the `GetAwaiter` protocol. ([Language Suggestion #840](https://github.com/fsharp/fslang-suggestions/issues/840), [PR #19785](https://github.com/dotnet/fsharp/pull/19785))
123 changes: 99 additions & 24 deletions src/FSharp.Core/async.fs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ open System.Runtime.ExceptionServices
open System.Threading
open System.Threading.Tasks
open Microsoft.FSharp.Core
open Microsoft.FSharp.Core.CompilerServices
open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicOperators
open Microsoft.FSharp.Control
open Microsoft.FSharp.Collections
Expand Down Expand Up @@ -1210,16 +1211,30 @@ module AsyncPrimitives =

task

// Used by Async.Await path to elide egregious AggregateException wrapping
[<DebuggerHidden>]
let UnwrapExn (exn: AggregateException) =
if exn.InnerExceptions.Count = 1 then
exn.InnerExceptions[0]
else
exn

// Call the appropriate continuation on completion of a task
[<DebuggerHidden>]
let OnTaskCompleted (completedTask: Task<'T>) (ctxt: AsyncActivation<'T>) =
let OnTaskCompleted unwrap (completedTask: Task<'T>) (ctxt: AsyncActivation<'T>) =
assert completedTask.IsCompleted

if completedTask.IsCanceled then
let edi = ExceptionDispatchInfo.Capture(TaskCanceledException completedTask)
ctxt.econt edi
elif completedTask.IsFaulted then
let edi = ExceptionDispatchInfo.RestoreOrCapture completedTask.Exception
let e =
if unwrap then
UnwrapExn completedTask.Exception
else
completedTask.Exception

let edi = ExceptionDispatchInfo.RestoreOrCapture e
ctxt.econt edi
else
ctxt.cont completedTask.Result
Expand All @@ -1229,14 +1244,20 @@ module AsyncPrimitives =
// the overall async (they may be governed by different cancellation tokens, or
// the task may not have a cancellation token at all).
[<DebuggerHidden>]
let OnUnitTaskCompleted (completedTask: Task) (ctxt: AsyncActivation<unit>) =
let OnUnitTaskCompleted unwrap (completedTask: Task) (ctxt: AsyncActivation<unit>) =
assert completedTask.IsCompleted

if completedTask.IsCanceled then
let edi = ExceptionDispatchInfo.Capture(TaskCanceledException(completedTask))
ctxt.econt edi
elif completedTask.IsFaulted then
let edi = ExceptionDispatchInfo.RestoreOrCapture completedTask.Exception
let e =
if unwrap then
UnwrapExn completedTask.Exception
else
completedTask.Exception

let edi = ExceptionDispatchInfo.RestoreOrCapture e
ctxt.econt edi
else
ctxt.cont ()
Expand All @@ -1246,10 +1267,10 @@ module AsyncPrimitives =
// completing the task. This will install a new trampoline on that thread and continue the
// execution of the async there.
[<DebuggerHidden>]
let AttachContinuationToTask (task: Task<'T>) (ctxt: AsyncActivation<'T>) =
let AttachContinuationToTask unwrap (task: Task<'T>) (ctxt: AsyncActivation<'T>) =
task.ContinueWith(
Action<Task<'T>>(fun completedTask ->
ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnTaskCompleted completedTask ctxt)
ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnTaskCompleted unwrap completedTask ctxt)
|> unfake),
TaskContinuationOptions.ExecuteSynchronously
)
Expand All @@ -1261,16 +1282,36 @@ module AsyncPrimitives =
// completing the task. This will install a new trampoline on that thread and continue the
// execution of the async there.
[<DebuggerHidden>]
let AttachContinuationToUnitTask (task: Task) (ctxt: AsyncActivation<unit>) =
let AttachContinuationToUnitTask unwrap (task: Task) (ctxt: AsyncActivation<unit>) =
task.ContinueWith(
Action<Task>(fun completedTask ->
ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnUnitTaskCompleted completedTask ctxt)
ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnUnitTaskCompleted unwrap completedTask ctxt)
|> unfake),
TaskContinuationOptions.ExecuteSynchronously
)
|> ignore
|> fake

let AwaitTask unwrap (task: Task<'T>) =
MakeAsyncWithCancelCheck(fun ctxt ->
if task.IsCompleted then
// Run synchronously without installing new trampoline
OnTaskCompleted unwrap task ctxt
else
// Continue asynchronously, via syncContext if necessary, installing new trampoline
let ctxt = DelimitSyncContext ctxt
ctxt.ProtectCode(fun () -> AttachContinuationToTask unwrap task ctxt))

let AwaitUnitTask unwrap (task: Task) =
MakeAsyncWithCancelCheck(fun ctxt ->
if task.IsCompleted then
// Continue synchronously without installing new trampoline
OnUnitTaskCompleted unwrap task ctxt
else
// Continue asynchronously, via syncContext if necessary, installing new trampoline
let ctxt = DelimitSyncContext ctxt
ctxt.ProtectCode(fun () -> AttachContinuationToUnitTask unwrap task ctxt))

/// Removes a registration places on a cancellation token
let DisposeCancellationRegistration (registration: byref<CancellationTokenRegistration option>) =
match registration with
Expand Down Expand Up @@ -2203,24 +2244,58 @@ type Async =
CreateWhenCancelledAsync compensation computation

static member AwaitTask(task: Task<'T>) : Async<'T> =
MakeAsyncWithCancelCheck(fun ctxt ->
if task.IsCompleted then
// Run synchronously without installing new trampoline
OnTaskCompleted task ctxt
else
// Continue asynchronously, via syncContext if necessary, installing new trampoline
let ctxt = DelimitSyncContext ctxt
ctxt.ProtectCode(fun () -> AttachContinuationToTask task ctxt))
AwaitTask false task

static member AwaitTask(task: Task) : Async<unit> =
MakeAsyncWithCancelCheck(fun ctxt ->
if task.IsCompleted then
// Continue synchronously without installing new trampoline
OnUnitTaskCompleted task ctxt
else
// Continue asynchronously, via syncContext if necessary, installing new trampoline
let ctxt = DelimitSyncContext ctxt
ctxt.ProtectCode(fun () -> AttachContinuationToUnitTask task ctxt))
AwaitUnitTask false task

static member Await(task: Task<'T>) : Async<'T> =
AwaitTask true task

static member Await(task: Task) : Async<unit> =
AwaitUnitTask true task

#if NETSTANDARD2_1
static member Await(task: ValueTask<'T>) : Async<'T> =
if task.IsCompletedSuccessfully then
CreateReturnAsync(task.GetAwaiter().GetResult())
else
AwaitTask true (task.AsTask())

static member Await(task: ValueTask) : Async<unit> =
if task.IsCompletedSuccessfully then
CreateReturnAsync(task.GetAwaiter().GetResult())
else
AwaitUnitTask true (task.AsTask())
#endif

module AsyncTaskLikeExtensions =

type Async with

[<NoEagerConstraintApplication>]
static member inline Await< ^TaskLike, ^Awaiter, 'T
when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter)
and ^Awaiter :> ICriticalNotifyCompletion
and ^Awaiter: (member get_IsCompleted: unit -> bool)
and ^Awaiter: (member GetResult: unit -> 'T)>
(task: ^TaskLike)
: Async<'T> =
Async.FromContinuations(fun (cont, econt, _ccont) ->
let mutable awaiter = (^TaskLike: (member GetAwaiter: unit -> ^Awaiter) task)

if (^Awaiter: (member get_IsCompleted: unit -> bool) awaiter) then
try
cont ((^Awaiter: (member GetResult: unit -> 'T) awaiter))
with e ->
econt e
else
(awaiter :> ICriticalNotifyCompletion)
.UnsafeOnCompleted(fun () ->
try
cont ((^Awaiter: (member GetResult: unit -> 'T) awaiter))
with e ->
econt e))

module CommonExtensions =

Expand Down
Loading
Loading