首先是一小部分背景信息。我正在使现有的C#库代码适合在WinRT上执行。作为这段代码的一小部分,内心深处需要做一个小文件IO,我们首先尝试保持同步并使用Task.Wait()来停止主线程直到所有IO完成。
果然,我们很快发现会导致陷入僵局。
然后我发现自己在原型中更改了很多代码,使其“异步”。也就是说,我插入了异步和等待关键字,我正在相应地更改方法返回类型。这是一项很多工作 - 实际上是太多无意义的工作 - 但是我让这个原型以这种方式工作。
然后我做了一个实验,并在一个单独的线程上使用Wait语句运行原始代码:
System.Threading.Tasks.Task.Run(()=> Draw(..., cancellationToken)
没有死锁!
现在我很困惑,因为我认为我理解异步编程是如何工作的。我们的代码(还)根本没有使用ConfigureAwait(false)。因此,所有等待语句应该在调用它们的相同上下文中继续。对吧?我假设这意味着:相同的线程。现在,如果此线程调用了“Wait”,这也会导致死锁。但事实并非如此。
你们中有没有明确坚如磐石的解释?
这个问题的答案将决定我是否真的会通过插入大量有条件的async / await关键字来搞乱我们的代码,或者我是否会保持干净并只使用一个在这里执行Wait()的线程那里。如果延续由任意非阻塞线程运行,那么事情应该没问题。但是,如果它们由UI线程运行,如果延续计算成本很高,我们可能会遇到麻烦。
我希望问题很清楚。如果没有,请告诉我。
答案 0 :(得分:7)
我的博客上有async
/await
intro,我在那里准确解释了上下文的含义:
它是SynchronizationContext.Current
,除非它是null
,在这种情况下它是TaskScheduler.Current
。注意:如果没有当前TaskScheduler
,则TaskScheduler.Current
与TaskScheduler.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
,它根本不应该看起来很乱。我不确定“有条件”是什么意思;我会把它全部async
。 await
有一个“快速路径”实现,如果操作已经完成,它将使其同步。
使用后台线程阻止异步代码是可能的,但它有一些警告:
await Task.Run(..)
,而不是Task.Run(..).Wait()
)。对于WinRT应用程序尤其如此。