Reputation: 5187
Assuming I have these two functions:
let dowork n =
async {
do printfn "work %d" n
}
let work i = async {
do! Async.Sleep(2000)
printfn "work finished %d" i }
How would I use Async.Parallel to run them concurrently and wait for both to finish before proceeding?
Upvotes: 20
Views: 6896
Reputation: 37045
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.
Upvotes: 2
Reputation: 1
let makeBoxed (job: Async<'a>) : Async<obj> =
async {
let! result = job
return box result
}
let mixedParallel2 (a: Async<'T1>) (b: Async<'T2>): Async<'T1*'T2> =
async {
let! results = Async.Parallel [| makeBoxed a ; makeBoxed b |]
return (unbox<'T1> results.[0]), (unbox<'T2> results.[1])
}
Unlike other answers, this one properly handles exceptions and cancellation. E.g. when one of the async computations raises exception, resulting combined computation exits immediately and raises that exception instead of waiting for other computations to finish.
Upvotes: 0
Reputation: 6008
As mentioned earlier, you just put your async functions in a sequence and pass them to Async.Parallel
.
But, if you need to execute different jobs that return results of different types, you can use Async.StartChild
:
let fn1 = async {
do! Async.Sleep 1000
printfn "fn1 finished!"
return 5
}
let fn2 = async {
do! Async.Sleep 1500
printfn "fn2 finished!"
return "a string"
}
let fncombined = async {
// start both computations simultaneously
let! fn1 = Async.StartChild fn1
let! fn2 = Async.StartChild fn2
// retrieve results
let! result1 = fn1
let! result2 = fn2
return sprintf "%d, %s" (result1 + 5) (result2.ToUpper())
}
fncombined
|> Async.RunSynchronously
|> printfn "%A"
Upvotes: 46
Reputation: 13862
Async.Parallel
takes a sequence of async. In this case I pass it a list.
[dowork 1; work 2]
|> Async.Parallel
|> Async.RunSynchronously
|> ignore
If you want to return different types of data use a Discriminated Union.
type WorkResults =
| DoWork of int
| Work of float32
let dowork n =
async {
do printfn "work %d" n
return DoWork(n)
}
let work i = async {
do! Async.Sleep(2000)
printfn "work finished %d" i
return Work(float32 i / 4.0f)
}
[dowork 1; work 2]
|> Async.Parallel
|> Async.RunSynchronously
|> printf "%A"
output
work 1
work finished 2
[|DoWork 1; Work 0.5f|]
Upvotes: 18