使用Async.Parallel合并结果而不重复昂贵的操作?

时间:2018-03-25 09:43:08

标签: asynchronous f#

假设我有一个同步昂贵的操作:

let SomeExpensiveOp():string=
    System.Console.WriteLine"about to begin expensive OP"
    System.Threading.Thread.Sleep(TimeSpan.FromSeconds 2.0)
    System.Console.WriteLine"finished expensive OP"
    "foo"

我将其作为异步作业包装:

let SomeExpensiveOpAs():Async<string>=async {
    return SomeExpensiveOp()}

现在我想用这个昂贵的操作将它与其他两个结合起来:

let SomeExpensiveOpSeq():seq<Async<string>>=
    let op = SomeExpensiveOpAs()
    seq {
        for some in [Bar(); Baz()] do
            yield async {
                let! prefix = op
                let! someAfterWaiting = some
                return (String.Concat (prefix, someAfterWaiting))
            }
    }

将其放入seq<Async<'T>>的目的是能够以这种方式使用Async.Parallel

let DoSomething() =
    let someAsyncOps = SomeExpensiveOpSeq() |> List.ofSeq
    let newOp = SomeExpensiveOpAs()
    let moreAsyncOps = (newOp::someAsyncOps)
    let allStrings = Async.RunSynchronously(Async.Parallel moreAsyncOps)
    for str in allStrings do
        Console.WriteLine str
    Console.WriteLine()

但是,这会使SomeExpensiveOp执行三次。由于上面的newOp调用,我希望第二次执行额外的时间,但我希望SomeExpensiveOpSeq重用对SomeExpensiveOp的调用而不是调用它两次。如何实现SomeExpensiveOpSeq只调用SomeExpensiveOp一次并将其重用于后续结果?

1 个答案:

答案 0 :(得分:2)

这里的关键观察是let!每次调用异步表达式 - 没有任何缓存其结果。考虑这个示例,我们有expOp : Async<string>,但我们async表达式中等待三次:

let expOp = SomeExpensiveOpAs()
async {
  let! a = expOp
  let! b = expOp
  let! c = expOp
  return [a;b;c]
} |> Async.RunSynchronously

about to begin expensive OP
finished expensive OP
about to begin expensive OP
finished expensive OP
about to begin expensive OP
finished expensive OP
val it : string list = ["foo"; "foo"; "foo"]

您可以看到每次都会评估async 昂贵的操作。如果您只想执行一次昂贵的操作,您可以完全评估/等待其结果并使用它而不是多次等待它:

let SomeExpensiveOpSeq():seq<Async<string>>=
    let op = SomeExpensiveOpAs() |> Async.RunSynchronously
    seq {
        for some in [Bar(); Baz()] do
            yield async {
                let! someAfterWaiting = some
                return (String.Concat (op, someAfterWaiting))
            }
    }

这仍然会导致昂贵的操作在您的代码中执行两次 - 一次在SomeExpensiveOpSeq,另一次由于被moreAsyncOps添加 - 但它可以进一步重构为单个调用。基本上,如果所有后续的异步操作依赖于这种昂贵的评估,为什么不先评估它一次,然后在必要时使用它的值:

let SomeExpensiveOpSeq op : seq<Async<string>>=
    seq {
        for some in [Bar(); Baz()] do
            yield async {
                let! someAfterWaiting = some
                return (String.Concat (op, someAfterWaiting))
            }
    }

let DoSomething() =
    let newOp = SomeExpensiveOpAs() |> Async.RunSynchronously
    let someAsyncOps = SomeExpensiveOpSeq newOp |> Async.Parallel |> Async.RunSynchronously
    let allStrings = newOp::(List.ofArray someAsyncOps)
    for str in allStrings do
        Console.WriteLine str
    Console.WriteLine()

> DoSomething();;
about to begin expensive OP
finished expensive OP
foo
foobar
foobaz