【问题标题】:Async.StartImmediate vs Async.RunSynchronouslyAsync.StartImmediate vs Async.RunSynchronously
【发布时间】:2016-09-17 12:38:19
【问题描述】:

根据我有限(甚至错误)的理解,Async.StartImmediate 和 Async.RunSynchronously 在当前线程上启动异步计算。那么这两个功能究竟有什么区别呢?谁能帮忙解释一下?

更新:

https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSharp.Core/control.fs 查看 F# 源代码后,我想我有点明白发生了什么。 Async.StartImmediate 在当前线程上启动异步。在它命中异步绑定之后,它是否会继续在当前线程上运行取决于异步绑定本身。例如,如果异步绑定调用 Async.SwitchToThreadPool,它将在 ThreadPool 而不是当前线程上运行。在这种情况下,如果您想返回当前线程,则需要调用 Async.SwitchToContext。否则,如果异步绑定没有做任何切换到其他线程,Async.StartImmediate 将继续在当前线程上执行异步绑定。在这种情况下,如果您只想停留在当前线程上,则无需调用 Async.SwitchToContext。

Dax Fohl 的示例之所以能在 GUI 线程上运行,是因为 Async.Sleep 仔细捕捉 SynchronizationContext.Current 并确保继续在捕获的上下文中运行,使用 SynchronizationContext.Post()。请参阅https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSharp.Core/control.fs#L1631,其中 unprotectedPrimitiveWithResync 包装器更改了“args.cont”(继续) 成为捕获上下文的 Post(请参阅:https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSharp.Core/control.fs#L1008 — trampolineHolder.Post 基本上是 SynchronizationContext.Post)。这只会工作 当 SynchronizationContext.Current 不为空时,GUI 线程总是如此。尤其, 如果你在控制台应用中使用 StartImmediate 运行,你会发现 Async.Sleep 确实会进入 ThreadPool,因为控制台应用中的主线程没有 SynchronizationContext.Current。

总而言之,这确实适用于 GUI 线程,因为某些函数(如 Async.Sleep、Async.AwaitWaitHandle 等)会仔细捕获并确保返回到先前的上下文。 看起来这是一种蓄意的行为,但是这似乎没有在 MSDN 中的任何地方记录。

【问题讨论】:

    标签: f#


    【解决方案1】:

    Async.RunSynchronously 等待整个计算完成。因此,当您需要从常规代码运行异步计算并需要等待结果时,请使用它。很简单。

    Async.StartImmediate 确保计算在当前上下文中运行,但不会等到整个表达式完成。最常见的用途(至少对我而言)是当您想在 GUI 线程上异步运行计算时。例如,如果你想以 1 秒的间隔在 GUI 线程上做三件事,你可以写

    async {
      do! Async.Sleep 1000
      doThing1()
      do! Async.Sleep 1000
      doThing2()
      do! Async.Sleep 1000
      doThing3()
    } |> Async.StartImmediate
    

    这将确保在 GUI 线程中调用所有内容(假设您从 GUI 线程 调用),但不会在整个 3 秒内阻塞 GUI 线程。如果你在那里使用RunSynchronously,它会在这段时间内阻塞GUI线程并且你的屏幕会变得无响应。

    (如果您还没有进行过 GUI 编程,那么请注意,对 GUI 控件的更新都必须从同一个线程中完成,这可能难以手动协调;以上内容消除了很多痛苦)。

    再举一个例子,这里:

    // Async.StartImmediate
    async {
      printfn "Running"
      do! Async.Sleep 1000
      printfn "Finished"
    } |> Async.StartImmediate
    printfn "Next"
    
    > Running
    > Next
    // 1 sec later
    > Finished
    
    // Async.RunSynchronously
    async {
      printfn "Running"
      do! Async.Sleep 1000
      printfn "Finished"
    } |> Async.RunSynchronously
    printfn "Next"
    
    > Running
    // 1 sec later
    > Finished
    > Next
    
    // Async.Start just for completion:
    async {
      printfn "Running"
      do! Async.Sleep 1000
      printfn "Finished"
    } |> Async.Start
    printfn "Next"
    
    > Next
    > Running // With possible race condition since they're two different threads.
    // 1 sec later
    > Finished
    

    还要注意Async.StartImmediate 不能返回值(因为在继续之前它不会运行到完成),而RunSynchronously 可以。

    【讨论】:

    • 很好的解释,但错误。 StartImmediate 仅确保异步计算在当前线程上开始。只要你有任何do!let!,异步就可以切换到任何其他线程。在 GUI 编程中,您需要使用 StartImmediate 开始异步获取当前上下文 let context = System.Threading.SynchronizationContext.Current,稍后您需要使用do! Async.SwitchToContext(context) 显式切换回 GUI 线程以对 GUI 进行一些更新。
    • @DavidRaab 你确定吗?我已经在长异步块中使用StartImmediate 而不使用SwitchToContext(除非我主动切换到线程池并返回)的生产应用程序至少三年并且从未有过交叉-线程共享冲突。我无法想象这只是运气。
    • @DavidRaab tomasp.net/blog/async-csharp-differences.aspx 的最后一部分似乎证实了我的立场。 StartImmediate 执行工作流同时保留同步上下文。授予任何do! flet! x = f 可以 做一些事情来明确地混淆你的上下文,但是如果f 的函数名称不能完全清楚,IMO 这是f 中的一个错误.在多年使用 GUI 和 F# Async 的过程中,我从来没有遇到过这种情况。
    • @DaxFohl 感谢您的精彩解释!我认为您的示例确实适用于 GUI 线程。但是,它的工作原理背后还有更多细节值得一提。请查看我更新的帖子。
    • @DaxFohl 该行为取决于当前同步上下文的配置方式。重要的是要了解通常像 WPF 这样的框架会设置上下文。如果没有设置上下文,它的行为就像我描述的那样。因此,它是否在 GUI 线程上执行取决于您使用的 GUI 框架。或者一般来说,如果任何框架设置了上下文。否则,没有任何上下文,它的行为就像我描述的那样。
    猜你喜欢
    • 2012-03-24
    • 1970-01-01
    • 1970-01-01
    • 2013-10-24
    • 1970-01-01
    • 2014-08-17
    • 2011-09-18
    • 1970-01-01
    相关资源
    最近更新 更多