我对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>>
签名。
第二个版本更灵活,这就是为什么它在标准库中。