A separate asynchronous computation-expression library, named Async2 and
async2 so it can be developed independently without conflicting with
FSharp.Core's Async and async.
The implementation uses the preview runtime-async compiler feature.
Async2<'T> is a cold carrier: its runtime-async Task<'T> factory receives
the CancellationToken supplied when the computation is started. Both library
projects target net11.0; the test projects use xUnit v3 with the Microsoft
Testing Platform runner.
The core library provides the async2 { ... } computation expression and
Async2 operations for starting, composing, and awaiting asynchronous work.
The builder accepts Async2, Task, ValueTask, and awaitable sources;
Async2.Sleep and other cold operations observe the token used to start the
computation.
open System.Threading
open Microsoft.FSharp.Control
let computation =
async2 {
do! Async2.Sleep 100
return 42
}
use cancellation = new CancellationTokenSource()
let result =
Async2.RunSynchronously(computation, cancellationToken = cancellation.Token)src/FSharp.Control.Async2 builds independently of the sequence library.
MailboxProcessor2<'Msg> is also part of this core library; see its section
below.
The repository pins a .NET 11 preview SDK in global.json and uses a local F#
compiler build with runtime-async support. The root Directory.Build.props
selects $(RuntimeAsyncBin)\fsc\Release\net11.0\fsc.dll and references
$(RuntimeAsyncBin)\FSharp.Core\Release\net10.0\FSharp.Core.dll.
RuntimeAsyncBin defaults to c:\dev\repos\fsharp\artifacts\bin; override
RuntimeAsyncBin or set RuntimeAsyncBitsPath to use artifacts from another
local F# checkout.
Build the solution and run its tests with:
dotnet build
dotnet testThe focused AsyncType, AsyncModule, and AsyncModuleFunctions suites are accompanied by direct xUnit ports of older compiler-library regressions. RuntimeAsync compiler-component tests are intentionally outside this project.
The benchmarks/FSharp.Control.Async2.Benchmarks project uses BenchmarkDotNet to measure runtime and allocations for recursive computations, cancellation, exception propagation, and per-level try/with and try/finally handling. Results are reported for recursion depths of 16, 256, and 2048. The recursive workloads await Task.Yield() every 16 levels, including the deepest level, to mix synchronous work with asynchronous suspension. Expected exceptions and cancellation are handled explicitly; unexpected failures are not swallowed.
Each workload has a built-in async counterpart with an Async method suffix.
Results are grouped by workload and depth, with async2 as each pair's baseline.
Both implementations start immediately and synchronously wait for completion:
Async2.RunSynchronouslyImmediate versus Async.StartImmediateAsTask followed
by GetAwaiter().GetResult(). Built-in async awaits Task.Yield() through
Async.FromContinuations, without an intermediate task per yield.
Built-in Async.StartImmediateAsTask does not preserve the cancellation token
in its canceled task, so its cancellation cases check that their token source
was canceled rather than requiring exception-token identity.
Build and run the benchmarks in Release mode with:
dotnet run --project benchmarks/FSharp.Control.Async2.Benchmarks/FSharp.Control.Async2.Benchmarks.fsproj -c ReleasePass BenchmarkDotNet arguments after --, for example -- --filter *DeepException*.
Run a single cold-start measurement of every case (eight workloads for both implementations at three depths, totaling 48 cases) as a quick failure/stack-overflow check:
dotnet run --project benchmarks/FSharp.Control.Async2.Benchmarks/FSharp.Control.Async2.Benchmarks.fsproj -c Release -- --filter "*" --job DryThe benchmark runner exits with a nonzero code if validation or a benchmark fails. Dry-job timings are smoke checks, not statistically reliable measurements.
MailboxProcessor2<'Msg> is an async2-native mailbox agent with Post,
Receive, TryReceive, Scan, TryScan, reply channels, cancellation,
Start, and StartImmediate. Its name and AsyncReplyChannel2<'Reply> keep
it distinct from FSharp.Core's MailboxProcessor and AsyncReplyChannel.
Its mailbox semantics follow FSharp.Core: only one reader operation may run
at a time, asynchronous reply methods post their message when called, and
Dispose clears the mailbox without canceling its body. Supply a cancellation
token explicitly when cancellable infinite waits are required.
type Message = | Add of int | Read of AsyncReplyChannel2<int>
let counter =
MailboxProcessor2<Message>.Start(fun inbox ->
let rec loop total =
async2 {
match! inbox.Receive() with
| Add value -> return! loop (total + value)
| Read reply ->
reply.Reply total
return! loop total
}
loop 0)
counter.Post(Add 42)
let total =
counter.PostAndAsyncReply((fun reply -> Read reply))
|> Async2.RunSynchronouslysrc/FSharp.Control.AsyncSeq2 remains a separate library referencing Async2,
so applications using only async2 do not take on the sequence API or its
packaging dependencies. It provides
asyncSeq2 { ... } in Microsoft.FSharp.Control, yielding AsyncSeq2<'T> (an
IAsyncEnumerable<'T>). It supports yield, yield!, for, let!/do! on
tasks, value tasks, and Async2, plus synchronous or asynchronous use.
The builder is adapted from FSharp.Control.TaskSeq (see src/FSharp.Control.AsyncSeq2/TASKSEQ-LICENSE.txt).
Each enumeration is cold; GetAsyncEnumerator(token) passes its token to
bound Async2 computations and nested asynchronous sequences. async2 can
consume an AsyncSeq2<'T> directly with for.
ofAsync2Seq, ofAsync2List, and ofAsync2Array turn collections of cold
Async2 computations into sequences; ofSeqAsync remains an equivalent
constructor for seq<Async2<'T>>.
AsyncSeq2 provides constructors, transformations, and the TaskSeq operation
surface, as well as AsyncSeq-derived operations such as cycle, rev,
splitInto, sortAsync, findBack, map2, allPairs, and unzip. The
AsyncSeq-derived operators and the TaskSeq-compatible terminal operations use
Async2 for asynchronous callbacks and cold terminal results; their
cancellation token flows implicitly to the enumerator. Existing Task and
ValueTask sources remain supported, but a previously started task cannot
retroactively inherit an enumeration token.
AsyncSeq2Src provides a broadcast source: toAsyncSeq subscribes at the
current tail, and put, close, or error notify existing subscribers.
See ASYNCSEQ-LICENSE.txt and TASKSEQ-LICENSE.txt for upstream attribution.
let values = asyncSeq2 {
for i in 1..3 do
do! Async2.Sleep 10
yield i
}
let result = AsyncSeq2.toArray values |> Async2.RunSynchronouslyFSharp.Control.AsyncSeq2.Tests contains xUnit v3/MTP builder, AsyncSeq
operator, cleanup, composition, and cancellation tests. It also ports all 494
active tests from FSharp.Control.AsyncSeq's AsyncSeqTests.fs, adapting
Async<'T> to Async2<'T> and accounting for the existing TaskSeq-compatible
API (including strict take/skip, truncate/drop, array-valued groups,
and removeManyAt range behavior). Async2.StartChild starts its child as
soon as the parent creates it, so bounded channel producers and consumers
can run concurrently. Disposing an enumerator from toBlockingSeq cancels
its enumeration token.
FSharp.Control.AsyncSeq2.TaskSeq.Tests ports all 74 non-smoke TaskSeq test
files (including two upstream-skipped cases) to xUnit v3/MTP. Transformation
tests call AsyncSeq2 directly with native async2 callbacks; xUnit test
boundaries and explicit Task and ValueTask interoperability tests retain
those carriers. The old F# Async<'T> sources and conversion helpers have
been replaced by Async2<'T>; use Async2 computations for cold, cancellable
operations.