jonnifyfsharp

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.

C1.09.2 · part 1

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 async block, awaits another Async<'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.
C1.09.2 · part 2

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 []

async { return 1 } async { return 2 } async { return 3 } Async.Parallel runs concurrently [|1; 2; 3|] build the values first; run them in one call
tasks |> Async.Parallel |> Async.RunSynchronously = [|1; 2; 3|] · type 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

0 100 200 300 ms task 1 task 2 task 3 total: 300 ms — task 1, then task 2, then task 3
mode sequential · three 100 ms workflows end to end · total 300 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

Related in this course