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