最近读到一篇异步转同步的文章,发现其中没有考虑到异步转同步过程中发生的死锁问题,所以特地在本文说说异步转同步过程中的死锁问题。


文章作者 林德熙 已经修复了描述:

  • win10 uwp 异步转同步

什么情况下会产生死锁?

调用 Task.Wait() 或者 Task.Result 立刻产生死锁的充分条件:

  1. 调用 Wait()Result 的代码位于 UI 线程;
  2. Task 的实际执行在其他线程,且需要返回 UI 线程。

死锁的原因:

UWP、WPF、Windows Forms 程序的 UI 线程都是单线程的。为了让使用了 async/await 的代码像使用同步代码一样简单,WPF 程序的 Application 类在构造的时候会将主 UI 线程 Task 的同步上下文设置为 DispatcherSynchronizationContext 的实例,这在我的另一篇文章 Task.Yield 中也有过说明。

Task 的任务结束时,会从 AsyncMethodStateMachine 中调用 AwaiterOnComplete() 方法,而 await 后续方法的执行靠的就是 OnComplete() 方法中一层层调用到 DispatcherSynchronizationContext 里的 Post 方法:

/// <summary>
///     Asynchronously invoke the callback in the SynchronizationContext.
/// </summary>
public override void Post(SendOrPostCallback d, Object state)
{// Call BeginInvoke with the cached priority.  Note that BeginInvoke// preserves the behavior of passing exceptions to// Dispatcher.UnhandledException unlike InvokeAsync.  This is// desireable because there is no way to await the call to Post, so// exceptions are hard to observe._dispatcher.BeginInvoke(_priority, d, state);
}

这里就是问题的关键!!!

如果 _dispatcher.BeginInvoke(_priority, d, state); 这句代码在后台线程,那么此时 UI 线程处于 Wait()/Result 调用中的阻塞状态,BeginInvoke 中的任务是无论如何也无法执行到的!于是无论如何都无法完成这个 Post 任务,即无论如何也无法退出此异步任务的执行,于是 Wait() 便无法完成等待……死锁……

这里给出最简复现的例子代码:

DoAsync().Wait();async Task DoAsync()
{await Task.Run(() => { });
}

无论是 WPF 还是 UWP,只要在 UI 线程上调用上述代码,必然死锁!

什么情况下不会产生死锁?

阅读了本文一开始说的那篇文章 win10 uwp 异步转同步 后,你一定好奇为什么此文的情况不会产生死锁。

那是因为,它不满足本文提到的充分条件——StorageFolder.GetFolderFromPathAsync("")StorageFolder.GetFolderFromPathAsync("") 这两个方法并不会在后台线程执行

逗我???不在后台线程执行怎么做到的异步等待!!!

是的,读写文件,访问网络,这些 IO 阻塞的操作执行时,里面根本就没有线程,详情请阅读:There Is No Thread。

还有另一些操作,也没有后台线程的参与,于是也不存在从后台线程回到主线程导致死锁的情况。如 Task.Yield,还有 InvokeAsync,它们也不会造成死锁。

另外,如果是控制台程序,或者一个普通的非 UI 线程,其 SynchronizationContext 为 null,那么异步任务执行完后不需要回到原有线程,也不会造成死锁。

总结不会造成死锁的充分条件:

  1. 异步操作执行完后不需要回到原有线程(例如非 UI 线程和控制台线程);
  2. 异步操作不需要单独的线程执行任务。

如何避免死锁?

明确了会造成死锁的条件和不会造成死锁的条件后,我们只需要做到以下几点即可避免死锁了:

  1. 在 UI 线程,如果使用了 async/await,就尽量不要再使用 Task.Wait()/Task.Result 了,就一直异步一条路走到黑好了(微软称其为 Async All the Way)。
  2. 如果可能,尽量在异步任务后添加 .ConfigureAwait(false);这样,异步任务后面继续执行的代码就不会回到原 UI 线程了,而是直接从线程池中再取出一个线程执行;这样,即便 UI 线程后续可能有别的原因造成阻塞,也不会产生死锁了。

把原来的代码改成这样,就不会死锁了:

await DoAsync();async Task DoAsync()
{await Task.Run(() => { });
}

没错!只能是一路 async/await。微软将其描述为:async/await 会像病毒一样在你的代码中传播。

Others have also noticed the spreading behavior of asynchronous programming and have called it “contagious” or compared it to a zombie virus.

这句话的原文参见:Async/Await - Best Practices in Asynchronous Programming

为了防止真的有代码的调用者使用 Wait(),我们也得写出防 SB 的代码来:

async Task DoAsync()
{await Task.Run(() => { }).ConfigureAwait(false);
}

这一句的目的是防止执行上下文切换回 UI 线程。

这样,即便真的使用 DoAsync().Wait() 也不会发生死锁。注意,整个方法调用链都需要使用 .ConfigureAwait(false) 才能够防止线程切换时,在调用方的 Wait() 方法中发生死锁。详见我的另一篇博客 在编写异步方法时,使用 ConfigureAwait(false) 避免使用者死锁。)

更多死锁问题

死锁问题:

  • 使用 Task.Wait()?立刻死锁(deadlock) - walterlv
  • 不要使用 Dispatcher.Invoke,因为它可能在你的延迟初始化 Lazy 中导致死锁 - walterlv
  • 在有 UI 线程参与的同步锁(如 AutoResetEvent)内部使用 await 可能导致死锁
  • .NET 中小心嵌套等待的 Task,它可能会耗尽你线程池的现有资源,出现类似死锁的情况 - walterlv

解决方法:

  • 在编写异步方法时,使用 ConfigureAwait(false) 避免使用者死锁 - walterlv
  • 将 async/await 异步代码转换为安全的不会死锁的同步代码(使用 PushFrame) - walterlv

参考资料

  • There Is No Thread
  • Async/Await - Best Practices in Asynchronous Programming

我的博客会首发于 https://walterlv.com/,而 CSDN 和博客园仅从其中摘选发布,而且一旦发布了就不再更新。

如果在博客看到有任何不懂的内容,欢迎交流。我搭建了 dotnet 职业技术学院 欢迎大家加入。

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:https://walterlv.blog.csdn.net/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。

使用 Task.Wait()?立刻死锁(deadlock)相关推荐

  1. ABAP面试题系列:写一组会出现死锁(Deadlock)的ABAP程序

    我们在计算机操作系统这门专业课上,学过死锁(Deadlock)的概念:两个或两个以上的进程(或线程)在执行过程中,由于竞争资源而造成的一种阻塞的现象,称为死锁.若无外力干预,这些处于死锁状态的进程将永 ...

  2. 什么是死锁(deadlock)?

    ● 请问什么是死锁(deadlock)? 考察点:线程死锁 参考回答: 两个线程或两个以上线程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁.结果就是这些线程都陷入了无限的等待中. 例如,如果 ...

  3. Java多线程学习之路(四)---死锁(DeadLock)

    Java多线程学习之路(四)-死锁(DeadLock) 1.定义 死锁就是多个线程在竞争共享资源的时候,相互阻塞,不能脱身的状态(个人理解).其实死锁一定程度上可以看成一个死循环. 举个现实生活中的例 ...

  4. 记mysql gone away和死锁Deadlock解决方案

    最近运行了两年的程序,在多个项目上出现了gone away和 Deadlock,下面记录下解决方案,以便以后查询. mysql gone away 可能原因 参考 http://ronaldbradf ...

  5. 又踩.NET Core的坑:在同步方法中调用异步方法Wait时发生死锁(deadlock)

    之前在将 Memcached 客户端 EnyimMemcached 迁移 .NET Core 时被这个"坑"坑的刻骨铭心(详见以下链接),当时以为只是在构造函数中调用异步方法(注: ...

  6. 一句 Task.Result 就死锁, 这代码还怎么写?

    一:背景 1. 讲故事 前些天把 .NET 高级调试 方面的文章索引到 https://github.com/ctripxchuang/dotnetfly 的过程中,发现了一个有意思的评论,截图如下: ...

  7. java jstack 死锁_利用jstack检测死锁DeadLock

    首先,制造一个死锁程序如下: public class testJstack { final static Object obj_1 = new Object(); final static Obje ...

  8. mysql deadlock found_mysql死锁 Deadlock found when trying to get lock; try restarting transaction

    研究了一下mysql的死锁,记录如下. 比如有2个事务,执行的sql分别如下: 这里用 #N 标识sql语句的执行顺序,下面开启两个mysql客户端连接,其中表的id为主键. 事务1 START TR ...

  9. 多线程,死锁,DeadLock

    /* 死锁. 同步中嵌套同步.*/class Ticket implements Runnable {private int tick = 1000;Object obj = new Object() ...

最新文章

  1. 面试:为什么 Java 线程没有Running状态?
  2. 参加IBM Rational高峰论坛的会议笔记
  3. Struts 拦截器权限控制【通过拦截器实现登录后跳转到登录前页面】
  4. msdn画圆弧函数_精确之美——用TikZ画硬盘示意图
  5. 备份linux系统报错_Linux 系统如何快速入门?分享民工哥总结的经验
  6. 如何得到发送邮件服务器地址(SMTP地址)
  7. 【演化计算】【论文研读】Surrogate-Assisted Evolutionary DL Using E2E Random Forest-based Performance Predictor
  8. MatLab基本知识学习 详细!
  9. 基于Java毕业设计医院药品管理系统源码+系统+mysql+lw文档+部署软件
  10. Geotools解析shp文件
  11. 关于EditPlus3 取消备份后再重新打开 备份设置还原到默认状态的问题
  12. kali下破解小区门禁卡实例
  13. java菜鸟疑问1:为什么我的代码总出现cannot be resolved or is not a field这种问题
  14. 360浏览器极速模式和兼容模式
  15. Python html.parser库学习小结
  16. 红楼梦中贾母评才子佳人戏--真正看的明白
  17. 第9天Sqltie数据库
  18. 字符转ASII码以及大小写之间的转换
  19. StackExchange.Redis Timeout awaiting response
  20. cmd中的循环语句for

热门文章

  1. 软考-软件设计师 UML建模
  2. 【间歇性努力,不是真正的努力】
  3. DingTalk「开发者说」|钉钉小程序开发实践
  4. Logo Grabber 一键快速下载网站Logo 的免费插件
  5. 【Java学习之代码学习】 Prog28_打印出杨辉三角形的问题
  6. 比尔·盖茨表示 AI应被用来改善教育医疗
  7. 字符函数和字符串函数
  8. python设计迷宫_用Python制作迷宫GIF
  9. vscode能写winform窗体吗_vs code 写C#心得
  10. 什么是业务流程重组?