Dive C1.09.2
Async workflows
An async { } block is a value — data, until something runs it.
The async { } computation expression describes asynchronous work as a
value of type Async<'T>. Building one runs nothing; it is a
recipe. Async.RunSynchronously runs a workflow and returns its result.
let! inside the block awaits another Async<'T>, and
Async.Parallel takes a list of workflows, runs them concurrently, and collects the
results into an array. Because a block is data, the same recipe can be run once, run later, or run in
parallel with others.
Work as a value
Binding async { return 1 + 2 } to a name creates a workflow but runs nothing — the
addition has not happened. Async.RunSynchronously is what runs it, returning the
result on the calling thread.
let work = async { return 1 + 2 } // val work : Async<int> — not run yet Async.RunSynchronously work // val it : int = 3
Three workflows in a list run one after another if each is run in turn, or concurrently with
Async.Parallel, which returns an array of their results in list order.
let tasks = [ async { return 1 }; async { return 2 }; async { return 3 } ] tasks |> Async.Parallel |> Async.RunSynchronously // val it : int [] = [|1; 2; 3|]
- async { }
- builds a value of type
Async<'T>— a recipe, not a running task. - let!
- inside an
asyncblock, awaits anotherAsync<'T>. - Async.RunSynchronously
- runs a workflow and returns its result.
- Async.Parallel
- runs a list of workflows concurrently; returns an array of results in order.
Build, then run
Three workflows are built, then handed to a runner. The split is the point: the building step produces
values, and the running step is a separate, explicit call. The diagram shows three
Async<int> values flowing into Async.Parallel and out as
one int [].
three Async<int> values · Async.Parallel · one int []
Building a workflow and running it are two steps. The first hands you a value; the second is the only place the work happens.
Sequential or parallel
Three workflows, each taking the same fixed slice of time. Run them sequentially and the
slices add up; run them with Async.Parallel and they overlap, so the total is the
longest single slice. The timing below is a fixed model — 100 ms per workflow, no real delay — so the
arithmetic is exact: three in a row is 300 ms, three at once is 100 ms. Toggle the two modes and read
the total.
three 100 ms workflows · sequential 300 ms vs parallel 100 ms
Parallel does not make each workflow faster; it overlaps them. Three independent 100 ms workflows finish in 100 ms together instead of 300 ms in a row.
A workflow is a value
Each async { } is built first and runs nothing on its own.
The runner sets the order
Async.Parallel overlaps the three; the total drops from 300 ms to 100 ms.
Where this leads
The async builder uses the same Bind shape the first dive
showed — let! awaits, then continues. The next dive is the lab: a builder you write
yourself, where you choose exactly what Bind does. Dive C1.09.3 builds an
OptionBuilder and watches a None short-circuit the chain.
References
Sources
- Microsoft — F# documentation — asynchronous programming with
asyncworkflows. - FSharp.Core API reference —
Async.RunSynchronouslyandAsync.Parallel. - Microsoft — .NET documentation — the runtime threading and asynchronous model F# builds on.
Related in this course
- C1.09.3 — Build your own — the lab: a builder written from scratch.
- C1.09 — Computation expressions — the module hub and its three dives.
- Elixir — Pipeline lab — the BEAM course's functional lab, composing work by hand.