为什么 Async.StartChild return `Async<Async<'T>>`?

Why does Async.StartChild return `Async<Async<'T>>`?

我是 F# 的新手,我一直在阅读 F# for Fun and Profit。在 Why use F#? 系列中,有一篇 post 描述了异步代码。我 运行 遍历了 Async.StartChild 函数,但我不明白为什么 return 值是这样。

例子:

let sleepWorkflow  = async {
    printfn "Starting sleep workflow at %O" DateTime.Now.TimeOfDay
    do! Async.Sleep 2000
    printfn "Finished sleep workflow at %O" DateTime.Now.TimeOfDay
}

let nestedWorkflow  = async {

    printfn "Starting parent"
    let! childWorkflow = Async.StartChild sleepWorkflow

    // give the child a chance and then keep working
    do! Async.Sleep 100
    printfn "Doing something useful while waiting "

    // block on the child
    let! result = childWorkflow

    // done
    printfn "Finished parent"
}

我的问题是为什么 Async.StartChild 不应该只是 return Async<'T> 而不是 Async<Async<'T>>?你必须在上面使用 let! 两次。 documentation 甚至声明:

This method should normally be used as the immediate right-hand-side of a let! binding in an F# asynchronous workflow [...] When used in this way, each use of StartChild starts an instance of childComputation and returns a completor object representing a computation to wait for the completion of the operation. When executed, the completor awaits the completion of childComputation.

在一些测试中,添加了一些睡眠调用,似乎没有初始 let! 子计算就永远不会启动。

为什么会有这种 return 类型/行为?我习惯了 C#,其中调用 async 方法总是会立即“启动”任务,即使您不 await 它也是如此。事实上,在 C# 中,如果 async 方法不调用任何异步代码,它将 运行 同步调用。

编辑澄清:

这样做有什么好处:

let! waiter = Async.StartChild otherComp // Start computation
// ...
let! result = waiter // Block

与 if Async.StartChild return 相比 Async<'T>:

let waiter = Async.StartChild otherComp // Start computation
// ...
let !result = waiter // Block

这个想法是这样的:你用 let wait = Async.StartChild otherComp 开始另一个异步计算(在后台)然后你得到 waiter 回来。

这意味着 let! result = waiter 阻止 并随时等待后台计算的结果。

如果 Async.StartChild 会 return a Async<'t> 你会 wait with let! x = otherComp 就像一个正常让!结果 = otherComp`


是的,F# 异步工作流只会 启动 一旦你做了类似 Async.Start...Async.RunSynchronously 的事情(它不像 Task通常在您创建它后立即运行)

这就是为什么在 C# 中您可以在某一点(即 Async.StartChild 部分)创建一个任务 (var task = CreateMyTask()),然后稍后使用 var result = await task 在那里等待结果(即 let! result = waiter 部分)。


为什么 Async.StartChild returns Async<Async<'T>> 而不是 Async<'T>

这是因为以这种方式启动的工作流应该表现得像 child-task/process。当您 取消 包含工作流时, 也应该 取消

因此,在技术层面上,子工作流需要访问取消令牌而无需您明确传递它,这是 Async-Type 在您使用 [= 时在后台为您处理的一件事26=](这里又名 let!)。

所以它必须是这种类型才能使取消令牌的传递起作用。

我一直在思考这个问题,但无法给出可靠的解释。事实上,作为概念证明,我能够编写具有您想要的行为的 StartChild 的粗略版本:

let myStartChild computation =

    let mutable resultOpt = None
    let handle = new ManualResetEvent(false)

    async {
        let! result = computation   // run the computation
        resultOpt <- Some result    // store the result
        handle.Set() |> ignore      // signal that the computation has completed
    } |> Async.Start

    async {
        handle.WaitOne() |> ignore   // wait for the signal
        handle.Dispose()             // cleanup
        return resultOpt.Value       // return the result
    }

我写了一个基本的冒烟测试,它似乎工作正常,所以我忽略了一些重要的事情(可能与取消令牌有关?),或者你的问题的答案是它没有就是那样。

无论如何我都不是 Async 专家,所以我希望有比我知识渊博的人来插话。

更新:根据 Carsten 更新的答案,我认为我们有一个完整的解释:您可以:

  • 有你想要的签名,但不支持取消,或者
  • 如果需要取消,请使用标准 Async<Async<'T>> 签名。

第二个版本更灵活,这就是它在标准库中的原因。