其实好久以前就看过这个文章,以及类似的很多篇文章。最近在和一个新同事的交流中发现原来对async的死锁理解不是很透彻,正好最近时间比较充裕就再当一回搬运工。

本文假定你对.NET Framework 4.5 的异步编程有基本的了解,相关的建议你能够在Stack Overflow, MSDN 以及async/awai FAQ中找到。这里并不试图想讲述多少新的知识点,而是想强调几点最佳实践,以减少你阅读大量文档排查问题的时间。本文的最佳实践更多的是一些指导意见,并不是实际意义上的规则。每条意见后面都有一些例外情况,这里都已经一一列出了,具体情况下面我们分条讲解。

规则

描述

例外

避免使用 async void

优先使用 async Task 而不用 async void

Event handlers

Async到顶

不要混合使用 blocking 和 async 的代码

Console main method

注意配置好执行的context

尽量设置 ConfigureAwait(false)

需要context的除外

避免使用 async void

Task 和 Task<T> 是异步方法的通常返回值类型。在将同步代码改造成async方式过程中,我们将void 类型返回值改成async Task.

Async/Await 最佳实践-LMLPHP

其实,这里你改写成async void 编译器不会报错。事实上,async void 存在的意义仅仅是使用在event handlers上。如果你在普通的代码中使用async void,该代码块中的异

常处理就会变得比较麻烦。

Async/Await 最佳实践-LMLPHP

可以看到,上面的Exception 无法被直接catch. 当然你可以使用AppDomain.UnhandledException等类似方法去捕捉该异常,但是这种代码维护起来就比较麻烦。另外,Task和Task<T>能够使用await, Task.WhenAny, Task.WhenAll等方式组合使用。Async Void 不行。同时Async void 方式比较难测试,具体原因可以参考原文。

让Async传染到顶。

这里可以称呼为async具有僵尸病毒般的传染性,async 会感染周围的代码,直到顶层。其实我们只需要顺其自然,让所有代码都传染上异步特性即可。如果我们使用Task.Wait 或者Task.Result去试图阻塞async 的传播,往往便会自找苦吃。这个往往是刚接触async 异步的人最容易犯的错误,这些人往往试图将async 方法做个小小包装搞成同步的方法,以达到不修改旧代码的目的。不幸的是,这样往往会导致死锁。这个死锁的问题在MSDN,StackOverFlow已经烂大街了。

Async/Await 最佳实践-LMLPHP

如上所示,重现这个死锁实在是太容易了。理解该死锁的原因在于理解await 处理contexts的方式。默认的,当一个未完成的Task 被await的时候,当前的上下文将在该Task完成的时候重新获得并继续执行剩余的代码。这个context就是当前的SynchronizationContext ,除非它是空的。GUI和ASP.NET 应用程序的SynchronizationContext 有排他性,只允许一个线程运行。当await 完成的时候,它试图在它原来的代码上下文执行它剩余的部分,但是该代码上下文中已经有一个线程在了,就是那个一直在同步等待async 完成的那个线程,它们两个相互等待,因此就死锁了。

注意控制台程序不会导致死锁。控制台的SynchronizationContext 是类似于一个线程池的机制而不是排他的。因此当await 结束的时候,它可以重新获得原来的上下文然后执行完剩余代码并返回成功。这个不同是很多人产生困惑的根源,当他们在控制台测试的时候程序跑的好好的,拷贝到GUI或者ASP.NET 程序中就发生了死锁。

因此,最佳的解决方案就是允许async 自动传染直到最上层,通常到入口点为止。控制台的Main 方法无法被标注成async,原因你懂的。因此Main 方法这里是个例外。

Async/Await 最佳实践-LMLPHP

通常全套异步需要做一些额外的工作,下面是一些必须做的额外工作。

不要使用

使用

需要获得值的时候

Task.Wait or Task.Result

await

需要等待任何一个任务

Task.WaitAny

await Task.WhenAny

需要等待所有任务完成

Task.WaitAll

await Task.WhenAll

需要等待

Thread.Sleep

await Task.Delay

   

当然如果你不打算全套使用async, 那么请必须注意处理好异常信息以及防止死锁的一些方法。

注意配置好执行的context

配置好continueOnCapturedContext为false可以提高性能。这个我建议直接看代码。

Async/Await 最佳实践-LMLPHP

我们将task 配置为不需要切换为原context, 这样后面的代码将不会在GUI 线程中运行,可以减少对性能的影响。除了性能有一点点提升以外,设置好context 还能够避免死锁。当然这种解决方式需要注意的点实在太多,稍不注意就又死锁了。

同步调用异步async防止死锁方案

因此网上有人给出了以下比较通用的解决方案。具体代码就不贴全了,大概思路就是对上下文的处理,以及对一些异常信息的包装等,需要代码的同学自己去翻。

Async/Await 最佳实践-LMLPHP

Async/Await 最佳实践-LMLPHP

Async/Await 最佳实践-LMLPHP

参考:

https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

05-06 12:03