问题 异步CTP - 推荐的任务调度方法


我目前正在开发一个使用TAP的大部分异步应用程序。每个有产卵方法的类 Task还有一个 TaskScheduler 注入其中。这允许我们执行任务的显式调度,据我所知,这不是Microsoft使用Async CTP的方式。

我对新方法(隐式调度)的唯一问题是我们以前的哲学一直是“我们知道延续将始终指定他们的任务调度程序,所以我们不需要担心我们完成任务的上下文” 。

远离它确实让我们感到担心,因为它在避免细微的线程错误方面效果非常好,因为对于每一段代码我们都可以看到编码器已经记住要考虑他所使用的线程。如果他们错过了指定任务调度程序,那就是一个错误。

问题1: 任何人都可以向我保证隐含的方法是个好主意吗?我看到ConfigureAwait引入了很多问题(false)以及传统/第三方代码中的显式调度。例如,我怎样才能确定我的'await-ridden'代码总是在UI线程上运行?

问题2: 所以,假设我们删除所有 TaskScheduler 从我们的代码DI开始使用隐式调度,我们如何设置默认的任务调度程序?如何在等待昂贵的方法之前通过方法中途更改调度程序,然后再将其重新设置?

(p.s.我已经读过了 http://msmvps.com/blogs/jon_skeet/archive/2010/11/02/configuring-waiting.aspx


4785
2018-01-06 15:36


起源



答案:


我会小心翼翼地回答。 ;)

问题1:任何人都可以向我保证隐含的方法是个好主意吗?我看到ConfigureAwait引入了很多问题(false)以及传统/第三方代码中的显式调度。例如,我怎样才能确定我的'await-ridden'代码总是在UI线程上运行?

规则 ConfigureAwait(false) 非常简单:如果您的方法的其余部分可以在线程池上运行,则使用它,如果您的方法的其余部分必须在给定的上下文中运行(例如,UI上下文),则不要使用它。

一般来说, ConfigureAwait(false) 应该由库代码使用,而不是由UI层代码(包括UI类型层,如MVVM中的ViewModels)使用。如果该方法是部分后台计算和部分UI更新,则应将其拆分为两种方法。

问题2:因此,假设我们从代码中删除所有TaskScheduler DI并开始使用隐式调度,那么我们如何设置默认任务调度程序?

async/await 通常不会使用 TaskScheduler;他们使用“调度上下文”概念。这实际上是 SynchronizationContext.Current,并回归 TaskScheduler.Current 只有没有 SynchronizationContext。因此,可以使用替换您自己的调度程序 SynchronizationContext.SetSynchronizationContext。你可以阅读更多关于 SynchronizationContext 在 这篇关于这个主题的MSDN文章

默认的调度上下文应该是几乎所有时间都需要的,这意味着您不需要乱用它。我只在进行单元测试或Console程序/ Win32服务时更改它。

如何在等待昂贵的方法之前通过方法中途更改调度程序,然后再将其重新设置?

如果你想做一个昂贵的操作(大概是在线程池上),那么 await 的结果 TaskEx.Run

如果您想因其他原因(例如,并发)更改调度程序,那么 await 的结果 TaskFactory.StartNew

在这两种情况下,方法(或委托)都在另一个调度程序上运行,然后该方法的其余部分将在其常规上下文中继续。

理想情况下,你想要每一个 async 存在于单个执行上下文中的方法。如果方法的不同部分需要不同的上下文,则将它们拆分为不同的方法。这条规则的唯一例外是 ConfigureAwait(false),允许方法在任意上下文中启动,然后在其执行的剩余时间内恢复到线程池上下文。 ConfigureAwait(false) 应该被视为一种优化(默认情况下是库代码),而不是设计理念。

以下是我的“Thread is Dead”演讲中的一些观点,我认为可以帮助您完成设计:

  • 遵循基于任务的异步模式指南。
  • 随着您的代码库变得更加异步,它本质上将变得更具功能性(与传统的面向对象相反)。这是正常的,应该被接受。
  • 随着您的代码库变得更加异步,共享内存并发逐渐演变​​为消息传递并发(即, ConcurrentExclusiveSchedulerPair 是新的 ReaderWriterLock)。

10
2018-01-06 17:43



很好的答案斯蒂芬,但澄清一下:如果在异步方法'A'内部使用了ConfigureAwait(false),那么如果我等待方法'A',那么我期望在哪个上下文?线程池,或者它会在等待对方法'A'的调用之前恢复原始上下文? - Lawrence Wagerfield
斯蒂芬的回答非常可靠。请注意,如果您在旧模型上设置了死机,则始终可以创建一个等待的自定义包装器(类似于ConfigureAwait()的工作方式),并将其作为Task / Task <T>上的扩展方法挂钩。例如,如果您的扩展方法名为ResumeOn(TaskScheduler ts),则代码可能如下所示:await Foo(...)。ResumeOn(ts);然后拥有与您自己的代码相同的调度语义,但具有“等待”带来的所有改进的流/执行优势。 - Theo Yaung
@Lawrence:异步方法的每个“层”都会传递其上下文,但不会上传。因此,如果 A 电话 ConfigureAwait(false),然后它将完成在线程池上运行。然后,什么时候 B 电话 await A(), 然后 B 将继续 它自己的 之后的原始语境 await。这个事实 A 在线程池上完成对剩余部分没有影响 B。 - Stephen Cleary


答案:


我会小心翼翼地回答。 ;)

问题1:任何人都可以向我保证隐含的方法是个好主意吗?我看到ConfigureAwait引入了很多问题(false)以及传统/第三方代码中的显式调度。例如,我怎样才能确定我的'await-ridden'代码总是在UI线程上运行?

规则 ConfigureAwait(false) 非常简单:如果您的方法的其余部分可以在线程池上运行,则使用它,如果您的方法的其余部分必须在给定的上下文中运行(例如,UI上下文),则不要使用它。

一般来说, ConfigureAwait(false) 应该由库代码使用,而不是由UI层代码(包括UI类型层,如MVVM中的ViewModels)使用。如果该方法是部分后台计算和部分UI更新,则应将其拆分为两种方法。

问题2:因此,假设我们从代码中删除所有TaskScheduler DI并开始使用隐式调度,那么我们如何设置默认任务调度程序?

async/await 通常不会使用 TaskScheduler;他们使用“调度上下文”概念。这实际上是 SynchronizationContext.Current,并回归 TaskScheduler.Current 只有没有 SynchronizationContext。因此,可以使用替换您自己的调度程序 SynchronizationContext.SetSynchronizationContext。你可以阅读更多关于 SynchronizationContext 在 这篇关于这个主题的MSDN文章

默认的调度上下文应该是几乎所有时间都需要的,这意味着您不需要乱用它。我只在进行单元测试或Console程序/ Win32服务时更改它。

如何在等待昂贵的方法之前通过方法中途更改调度程序,然后再将其重新设置?

如果你想做一个昂贵的操作(大概是在线程池上),那么 await 的结果 TaskEx.Run

如果您想因其他原因(例如,并发)更改调度程序,那么 await 的结果 TaskFactory.StartNew

在这两种情况下,方法(或委托)都在另一个调度程序上运行,然后该方法的其余部分将在其常规上下文中继续。

理想情况下,你想要每一个 async 存在于单个执行上下文中的方法。如果方法的不同部分需要不同的上下文,则将它们拆分为不同的方法。这条规则的唯一例外是 ConfigureAwait(false),允许方法在任意上下文中启动,然后在其执行的剩余时间内恢复到线程池上下文。 ConfigureAwait(false) 应该被视为一种优化(默认情况下是库代码),而不是设计理念。

以下是我的“Thread is Dead”演讲中的一些观点,我认为可以帮助您完成设计:

  • 遵循基于任务的异步模式指南。
  • 随着您的代码库变得更加异步,它本质上将变得更具功能性(与传统的面向对象相反)。这是正常的,应该被接受。
  • 随着您的代码库变得更加异步,共享内存并发逐渐演变​​为消息传递并发(即, ConcurrentExclusiveSchedulerPair 是新的 ReaderWriterLock)。

10
2018-01-06 17:43



很好的答案斯蒂芬,但澄清一下:如果在异步方法'A'内部使用了ConfigureAwait(false),那么如果我等待方法'A',那么我期望在哪个上下文?线程池,或者它会在等待对方法'A'的调用之前恢复原始上下文? - Lawrence Wagerfield
斯蒂芬的回答非常可靠。请注意,如果您在旧模型上设置了死机,则始终可以创建一个等待的自定义包装器(类似于ConfigureAwait()的工作方式),并将其作为Task / Task <T>上的扩展方法挂钩。例如,如果您的扩展方法名为ResumeOn(TaskScheduler ts),则代码可能如下所示:await Foo(...)。ResumeOn(ts);然后拥有与您自己的代码相同的调度语义,但具有“等待”带来的所有改进的流/执行优势。 - Theo Yaung
@Lawrence:异步方法的每个“层”都会传递其上下文,但不会上传。因此,如果 A 电话 ConfigureAwait(false),然后它将完成在线程池上运行。然后,什么时候 B 电话 await A(), 然后 B 将继续 它自己的 之后的原始语境 await。这个事实 A 在线程池上完成对剩余部分没有影响 B。 - Stephen Cleary