Note: I have updated this answer after discovering a flaw in the original solution (see below)
When the number of tasks is fixed at compile-time, I like to have a helper function like this:
module Async =
let zip (a : Async<'a>) (b : Async<'b>) : Async<'a * 'b> =
async {
let! xs =
Async.Parallel
[|
async {
let! x = a
return box x
}
async {
let! x = b
return box x
}
|]
let i = unbox xs[0]
let j = unbox xs[1]
return i, j
}
Usage like this:
let work1 = async { return 1 }
let work2 = async { return "a" }
let work1And2 =
async {
let! (a, b) = Async.zip work1 work2
printfn "%i %s" a b
}
Note how the tasks are of different types. This can be very useful!
We can add an extra helper for Async<unit>, since () and ((), ()) have the same semantics:
module Async =
// ...
let doZip (a : Async<unit>) (b : Async<unit>) =
zip a b
|> Async.Ignore
Then applied to your scenario:
async {
do! Async.doZip (dowork 1) (work 2)
}
New in F# is applicatives. With these, we can extend the async computation expression to support a parallel and! keyword!
Here is the extension:
type AsyncBuilder with
member this.Bind2(a, b, f) =
async {
let! c = Async.zip a b
return f c
}
Usage like this:
let work1And2 =
async {
let! a = work1
and! b = work2
printfn "%i %s" a b
}
The old answer was to use Async.StartChild:
module Async =
let zip a b =
async {
// Start both tasks
let! x = Async.StartChild a
let! y = Async.StartChild b
// Wait for both to finish
let! i = x
let! j = y
// Return both results as a strongly-typed tuple
return i, j
}
The downside of this approach is that it can be slow to fail. Consider:
let workflow =
Async.zip
(async {
do! Async.Sleep 5_000
return "abc"
})
(async {
failwith "Kaboom"
return "def"
})
This fails after 5 seconds, when it could fail immediately.