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

3
我对F#非常陌生,目前正在阅读《F# for Fun and Profit》。在“为什么使用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 都会启动 childComputation 的实例,并返回一个表示等待完成操作的计算器对象。执行时,该计算器等待 childComputation 完成。

在某些测试中,添加了一些休眠调用,似乎没有初始的 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
2个回答

2
这个想法是这样的:您使用 let wait = Async.StartChild otherComp 在后台启动另一个异步计算,并获得等待者返回。
这意味着 let!result = waiter 将被阻塞,并且在您希望时等待后台计算的结果。
如果 Async.StartChild 返回一个 Async<'t>,则您会在那里使用 let! x = otherComp 等待,并且这将与一个普通let! result = otherComp`一样。
是的,F# Async-Workflows只有在执行Async.Start...Async.RunSynchronously时才会开始(而不像一个Task通常在创建后立即运行)。
这就是为什么在C#中,您可以在某一点上创建任务( var task = CreateMyTask() ),然后稍后使用 var result = await task 等待结果(这是 let!result = waiter 部分)。

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

这是因为以这种方式启动的工作流应该像一个-任务/进程一样运行。当您取消包含的工作流时,应该同时取消子级。
因此,在技术层面上,需要子工作流访问取消令牌而无需显式传递它,这就是 Async 类型在使用 Bind (即此处的 let!)时为您在后台处理的内容之一。
所以必须是这种类型才能使取消标记的传递工作。

谢谢您的回复,我还是有点困惑,您能否编辑一下以回答我的澄清问题? - Adam
1
希望这能对你有所帮助。 - Random Dev
2
真巧!我正在输入一个回答,提供了OP想要的签名,并猜测也许实际的签名是由于取消要求...就在你添加解释的时候。 :) - Brian Berns
@Carsten,非常感谢您的帮助。如果我可以说出一些我只了解一半的语言,那么let!是一个单子绑定,就像Haskell中的<-一样,所以它必须返回该单子包装值的上下文,但在这种情况下,该值本身是一个Async<'T>,所以我们需要我们所需的返回值。对吗? - Adam
1
不知道我是否正确理解了问题 - 我想说的是,您希望子工作流共享在 F# 的 Async<'T> 类型中隐藏的取消令牌。计算/异步工作流的实现方式将该令牌传递到使用 ! 的部分(是的,这些大多数被翻译为绑定) - 因此,这不完全是因为返回类型,而是因为您希望发生 令牌传递 - 如果您想在 C# 中实现这一点,则必须自己传递令牌(据我所知)。 - Random Dev
@Carsten 是的!我认为我们在同一页面上,感谢您的帮助。Bind 隐藏了实现方式,就像 Haskell 的 >>=(也是绑定)运算符隐藏了额外的数据一样。原始的 Async<'T> 处于一个上下文中。 - Adam

1
我已经思考了一段时间,但无法给出一个确切的解释。实际上,作为概念证明,我能够编写一个粗糙版本的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> 签名。
第二个版本更加灵活,这就是为什么它在标准库中的原因。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接