Async and Task
Prerequisites: Computation expressions
Programs spend much of their time waiting: for a web request, a database, a file. Asynchronous code lets a thread do other work while it waits. F# has two ways to write it:
task { }— produces a .NETTask<'T>, the same type C#asyncmethods use. It starts running immediately ("hot"). Use it for ASP.NET Core, most .NET libraries, and performance-sensitive code.async { }— produces an F#Async<'T>, a description of work that runs only when you start it ("cold"). It has built-in cancellation and composes nicely.
task { }
open System.Threading.Tasks
let fetchPrice (product: string) =
task {
do! Task.Delay 100 // simulate a slow network call
return product.Length * 10
}
let total =
task {
let! tea = fetchPrice "tea"
let! coffee = fetchPrice "coffee"
return tea + coffee
}
printfn "%d" total.Result
90
let! awaits a task; do! awaits one that returns nothing. .Result
blocks until the task finishes — fine at the top of a script, but in
real applications you let! all the way up and let the framework wait.
Running tasks in parallel
The two calls above run one after the other (about 200 ms in total). To run independent work at the same time, start all the tasks first, then wait for them together:
open System.Diagnostics
open System.Threading.Tasks
let work (name: string) (ms: int) =
task {
do! Task.Delay ms
return $"{name} done"
}
let sw = Stopwatch.StartNew()
let results =
[ work "A" 300; work "B" 300; work "C" 300 ]
|> Task.WhenAll
let all = results.Result
printfn "%A" all
printfn "took under 600 ms: %b" (sw.ElapsedMilliseconds < 600L)
[|"A done"; "B done"; "C done"|]
took under 600 ms: true
Three 300 ms operations finished in roughly 300 ms, not 900.
async { }
let download (name: string) =
async {
do! Async.Sleep 100
return name.ToUpper()
}
let job = download "report" // nothing has run yet
printfn "created the job"
printfn "%s" (Async.RunSynchronously job)
let all =
[ "a"; "b"; "c" ]
|> List.map download
|> Async.Parallel
|> Async.RunSynchronously
printfn "%A" all
created the job
REPORT
[|"A"; "B"; "C"|]
Async.Parallelruns a list of asyncs concurrently and collects the results in order.Async.Parallel(jobs, maxDegreeOfParallelism = 4)limits how many run at once.Async.RunSynchronouslystarts an async and waits for it;Async.Startstarts it without waiting.Async.AwaitTaskturns aTaskinto anAsync;Async.StartAsTaskgoes the other way.
Cancellation
Async workflows check for cancellation automatically at every let!
and do!:
open System.Threading
let slowCount =
async {
for i in 1 .. 10 do
do! Async.Sleep 200
printfn "tick %d" i
}
let cts = new CancellationTokenSource(500) // cancel after 500 ms
try
Async.RunSynchronously(slowCount, cancellationToken = cts.Token)
with :? System.OperationCanceledException ->
printfn "cancelled"
tick 1
tick 2
cancelled
For task { }, pass a CancellationToken explicitly to the methods you
call (Task.Delay(ms, token), httpClient.GetAsync(url, token)).
A real HTTP call
// illustrative: needs network access
open System.Net.Http
let client = new HttpClient()
let pageSize (url: string) =
task {
let! html = client.GetStringAsync url
return url, html.Length
}
let sizes =
[ "https://fsharp.org"; "https://dotnet.microsoft.com" ]
|> List.map pageSize
|> System.Threading.Tasks.Task.WhenAll
for (url, size) in sizes.Result do
printfn "%s: %d characters" url size
Create one HttpClient and reuse it — creating one per request can
exhaust network sockets under load.
Which one should I use?
Use task { } by default when working with .NET libraries and web
frameworks. Use async { } when you want cold, composable workflows
with implicit cancellation — for example, in agents (next lesson) and
background jobs.
Common mistakes
- Calling
.ResultorAsync.RunSynchronouslydeep inside a web handler — it blocks a thread and can deadlock or starve the server. Stay asynchronous all the way down. - Forgetting that a task starts immediately: creating tasks in a list already runs them.