为什么task.Wait在非ui线程上没有死锁?还是我很幸运?

时间:2013-08-15 10:21:36

标签: c# asynchronous windows-runtime deadlock async-await

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

果然,我们很快发现会导致陷入僵局。

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

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

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

没有死锁!

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

你们中有没有明确坚如磐石的解释?

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

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

1 个答案:

答案 0 :(得分:7)

我的博客上有async/await intro,我在那里准确解释了上下文的含义:

它是SynchronizationContext.Current,除非它是null,在这种情况下它是TaskScheduler.Current。注意:如果没有当前TaskScheduler,则TaskScheduler.CurrentTaskScheduler.Default相同,后者是线程池任务调度程序。

在今天的代码中,通常只取决于你是否有SynchronizationContext;任务调度程序今天没有被大量使用(但将来可能会变得更常见)。我有一个article on SynchronizationContext来描述它是如何工作的以及.NET提供的一些实现。

WinRT和其他UI框架(WinForms,WPF,Silverlight)都为其主UI线程提供SynchronizationContext。此上下文仅表示单个线程,因此如果混合阻塞和异步代码,则可以快速遇到死锁。我更详细地描述了为什么会发生这种情况in a blog post,但总结一下,它死锁的原因是因为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应用程序尤其如此。