6

首先是一点背景信息。我正在使现有的 C# 库代码适合在 WinRT 上执行。由于这段代码的一小部分需要做一些文件 IO,我们首先尝试保持同步并使用 Task.Wait() 来停止主线程,直到所有 IO 完成。

果然,我们很快就发现导致了死锁。

然后我发现自己在原型中更改了很多代码以使其“异步”。也就是说,我插入了 async 和 await 关键字,并相应地更改了方法返回类型。这是很多工作——事实上,太多毫无意义的工作——但我让原型以这种方式工作。

然后我做了一个实验,我在一个单独的线程上运行了带有 Wait 语句的原始代码:

System.Threading.Tasks.Task.Run(()=> Draw(..., cancellationToken)

没有僵局!

现在我很困惑,因为我认为我了解异步编程的工作原理。我们的代码(还)根本没有使用 ConfigureAwait(false)。所以所有 await 语句都应该在它们被调用时在相同的上下文中继续。对吗?我认为这意味着:相同的线程。现在如果这个线程调用了“Wait”,这也应该导致死锁。但事实并非如此。

你们中的任何人都有明确的坚如磐石的解释吗?

这个问题的答案将决定我是否真的会通过插入大量条件 async/await 关键字来搞乱我们的代码,或者我是否会保持它干净并只使用一个在这里和那里执行 Wait() 的线程。如果延续由任意非阻塞线程运行,那么事情应该没问题。但是,如果它们由 UI 线程运行,那么如果延续的计算成本很高,我们可能会遇到麻烦。

我希望这个问题很清楚。如果没有,请告诉我。

4

1 回答 1

7

我的博客上有一个async/await介绍,我在其中准确解释了上下文是什么:

它是SynchronizationContext.Current,除非它是null,在这种情况下它是TaskScheduler.Current。注意:如果没有 current TaskScheduler,则TaskScheduler.Current与 相同TaskScheduler.Default,为线程池任务调度器。

在今天的代码中,它通常归结为你是否有一个SynchronizationContext; 任务调度程序今天并没有被大量使用(但将来可能会变得更加普遍)。我有一篇文章SynchronizationContext描述了它的工作原理以及 .NET 提供的一些实现。

WinRT 和其他 UI 框架(WinForms、WPF、Silverlight)都SynchronizationContext为其主 UI 线程提供了一个。这个上下文只代表一个线程,所以如果你混合使用阻塞和异步代码,你很快就会遇到死锁。我在一篇博文中更详细地描述了为什么会发生这种情况,但总而言之,它死锁的原因是因为该async方法试图重新进入它SynchronizationContext(在这种情况下,恢复在 UI 线程上执行),但 UI 线程是阻塞等待该async方法完成。

线程池没有SynchronizationContext(或TaskScheduler,通常)。因此,如果您在线程池线程上执行并阻塞异步代码,它不会死锁。这是因为捕获的上下文是线程池上下文(不绑定到特定线程),因此该async方法可以重新进入其上下文(仅通过在线程池线程上运行)而另一个线程池线程被阻塞等待它来完成。

这个问题的答案将决定我是否真的会通过插入大量条件 async/await 关键字来搞乱我们的代码,或者我是否会保持它干净并只使用一个在这里和那里执行 Wait() 的线程。

如果你的代码是async一路的,它不应该看起来很乱。我不确定您所说的“有条件”是什么意思;我会全力以赴asyncawait有一个“快速路径”实现,如果操作已经完成,它会使其同步。

使用后台线程阻塞异步代码是可能的,但它有一些警告:

  1. 你没有 UI 上下文,所以你不能做很多 UI 的事情。
  2. 您仍然必须“同步”到 UI 线程,并且您的 UI 线程不应该阻塞(例如,它应该await Task.Run(..),不应该Task.Run(..).Wait())。对于 WinRT 应用程序尤其如此。
于 2013-08-15T12:30:13.717 回答