JonnyBoats
JonnyBoats

Reputation: 5187

F# using Async.Parallel to run 2 tasks in parallel

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

Answers (4)

sdgfsdh
sdgfsdh

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

webwarrior
webwarrior

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

MisterMetaphor
MisterMetaphor

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

gradbot
gradbot

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

Related Questions