为什么 Async.StartChild 返回 'Async<Async<'T>>'?



我对F#很陌生,我一直在阅读F#以获得乐趣和利润。在为什么使用 F#?系列中,有一篇描述异步代码的文章。我遇到了Async.StartChild函数,我不明白为什么返回值是什么。

示例:

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不应该只返回Async<'T>而不是Async<Async<'T>>?您必须在其上使用两次let!。文档甚至指出:

此方法通常应用作 F# 异步工作流中 let! 绑定的右侧 [...]以这种方式使用时,每次使用 StartChild 都会启动 childComputing 的一个实例,并返回一个表示计算的 completor 对象,以等待操作完成。执行时,完成器等待子计算完成。

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

为什么有这种返回类型/行为?我习惯了 C#,其中调用async方法将始终立即"启动"任务,即使您不await它。实际上,在 C# 中,如果async方法不调用任何异步代码,它将同步运行。

编辑澄清:

这样做的好处是什么:

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

Async.StartChild返回Async<'T>相比:

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

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

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

如果Async.StartChild会返回一个Async<'t>你会在那里等待let! x = otherComp,就像一个普通的让! 结果 = 其他补偿'


是的,F# 异步工作流只有在您执行Async.Start...Async.RunSynchronously之类的操作后才会启动(它不像通常在创建后立即运行的Task)

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


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

这是因为以这种方式启动的工作流应该像任务/进程一样运行。取消包含工作流时,子工作流也应取消

因此,在技术层面上,子工作流需要访问取消令牌,而无需显式传递它,这是当您使用Bind时,Async-Type 会在后台为您处理一件事(此处又名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专家,所以我希望有一个比我更有知识的人来插话。

更新:根据卡斯滕更新的答案,我认为我们有一个完整的解释:你可以:

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

第二个版本更灵活,这就是为什么它在标准库中。

最新更新