在我们 2015 年开始的从 .NET Framework 向 .NET Core 迁移的工程中,遇到的最大的坑就是标题中所说的——同步方法中调用异步方法发生”死锁”。虽然在 .NET Framework 时代就知道不能在同步方法中调用异步方法,但我们却明知路有坑,偏向此路行。不是我们自讨苦吃,而是被迫无奈,因为在 .NET Core 2.0 之前,BCL(基础类库)中有些 API 只有异步实现没有同步实现,比如用于将主机名解析为 IP 地址的 API —— Dns.GetHostAddressesAsync() 。

但最终“被迫无奈”变成“血的教训”,这根本不是坑,而是无底洞。无论在开发与测试环境中多么正常,只要一发布到生产环境有一定并发量就会发生“死锁” —— 大量请求无响应,一直处于等待状态,线程池发飙,线程数持续不断地增长,内存随之增长,直至撑爆服务器(详见当时的一篇随笔 .NET Core 中遇到奇怪的线程死锁问题:内存与线程数不停地增长)。

我们想尽一切方法,用尽网上能找到的同步方法调用异步方法避免死锁的办法,都于事无补,唯有去掉同步方法调用异步方法的代码。当我们意识这是一个无底洞后,赶紧绕道而行,全面放弃在同步方法中调用异步方法,并将“千万千万不要在同步方法中调用异步方法”作为一条 .NET Core 开发准则。

这段踩坑踩到无底洞的血泪史,每当想起都很心痛,心痛不是当时的任何努力都是那么的苍白无力,而是对问题背后原因的困惑 —— 为什么同步方法中 Wait 异步方法会产生如此致命的后果?如果真的千万千万不能这么干,那 .NET Core 为什么不直接在编译时就报错?“死锁”的背后究竟发生了什么?

。。。

2018年10月20日偶然间发现一个网站 —— dotNET Weekly ,在其中发现一篇10月17日发布的博文 —— .NET Threadpool starvation, and how queuing makes it worse,在读懂这篇博文之后,联系到之前踩坑的经历,终于想通了“死锁”的背后(只是个人推测,并不一定正确)。

.NET Core 线程池有 n+1 个队列,每个线程有自己的本地队列(n),整个线程池有一个全局队列(1)。每个线程接活(从队列中取出任务执行)的顺序是这样的:先从自己的本地队列中找活 -> 如果本地队列为空,则从全局队列中找活 -> 如果全局队列为空,则从其他线程的本地队列中抢活。

我们来想象一下异步方法等待同步方法的场景。当10个并发请求到达时(进入的是全局队列),假设线程池中正好有10个空闲线程,这10个线程立马把活接过来,但线程在执行过程中遇到了同步方法等待异步方法(Task.Wait)的情况而进入阻塞状态,无奈地无所事事地在那干等异步方法执行完成而无法帮其他线程干活(这时情况已经有些不妙,由于阻塞线程池少了10个干活的线程)。雪上加霜的是,这些阻塞的线程所等待的异步方法在完成异步操作执行 await 之后的代码时也需要线程,不仅干活的线程少了,而且剩下的线程要干的活更多了(情况更不妙了)。随着并发请求持续不断地进来,形势变得越来越严峻,被阻塞的线程越来越多,能干活的线程越来越少而且要干的活越来越多,于是越来越多的一线干活的线程的队列开始排起了长队。火上浇油的是,那些阻塞着的线程要退出阻塞状态需要等它们所等待的任务被正忙得不可开交的干活线程执行,干活线程越忙,它们被阻塞的时间越长。于是出现了一个奇怪的场面,一群不干活的线程围观并等待着少数干活的线程,眼看着这些干活线程的队列排队越来越长,虽然它们也能干活,但由于它们被关在小黑屋里,无法出手相助,要等它们的主人将它们释放出来,而它们的主人就排在长队中等着从干活线程那拿到小黑屋的钥匙。。。这样的场面最终只有一个结局,所有干活的线程的本地队列都排起了长队,没有空闲的线程。

好戏开始了,不,是灾难开始了。线程池中没有空闲线程,全局队列中的活没人接,于是全局队列开始排队,线程池的线程不够用,如果不赶紧补充线程进来,线程池会被饿死(Threadpool Starvation)。救援行动开始了,CLR 赶紧生产线程喂给线程池,由于全局队列享有最高优先级(根据之前所述的线程接活顺序),一喂进去就被全局队列吃了,但 CLR 一秒钟只能生产1-2个线程,远远满足不了全局队列的胃口,而最需要救援的各个干活线程的本地队列连汤都喝不到。除了 CLR 的外部救援,线程池也同时进行自救,有些线程玩命干活,终于处理完了自己队列中的任务,终于有机会可以帮助其他同伴了,但是它们立即接到了上级命令 —— 以最快速度去救援全局队列,军令不可违,它们眼睁睁地看着同伴绝望地处理着一望无际的长队中的任务,奔赴全局队列,自救也救不到干活线程的本地队列。

这种完全以全局队列为中心、救地位最高的、不救最需要的救援行动最终带来了毁灭性的结果。那些解救全局队列的线程又因为 Task.Wait 而阻塞而需要更多的线程执行阻塞所等待的任务。救援行动变成了自杀行动,线程池就这样被活活饿死了(Threadpool Starvation)。

这就是我所推测的真相,真相背后的真正罪魁祸首其实是对线程的阻塞,所以千万千万不要阻塞(blocking)线程。

原文地址:https://www.cnblogs.com/dudu/p/9860959.html

.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

一码阻塞,万码等待:ASP.NET Core 同步方法调用异步方法“死锁”的真相相关推荐

  1. 【源码解读】Vue与ASP.NET Core WebAPI的集成

    在前面博文[Vue]Vue 与 ASP.NET Core WebAPI 的集成中,介绍了集成原理:在中间件管道中注册SPA终端中间件,整个注册过程中,终端中间件会调用node,执行npm start命 ...

  2. Dapr 运用之集成 Asp.Net Core Grpc 调用篇

    前置条件: <Dapr 运用> 改造 ProductService 以提供 gRPC 服务 从 NuGet 或程序包管理控制台安装 gRPC 服务必须的包 Grpc.AspNetCore ...

  3. ASP.NET Core 2调用Azure云上的PowerBI报表展示

    在开发企业应用中,报表功能是当之无愧的重头戏,如何将数据通过合适的报表呈现出来成为每个项目人员必需面临的问题.而找到一款合适的报表往往都需要考率价格.开发.风格.支撑等因素.那么,我在这里给大家介绍一 ...

  4. ASP.NET Core 源码学习之 Logging[2]:Configure

    在上一章中,我们对 ASP.NET Logging 系统做了一个整体的介绍,而在本章中则开始从最基本的配置开始,逐步深入到源码当中去. 默认配置 在 ASP.NET Core 2.0 中,对默认配置做 ...

  5. ASP.NET Core 源码学习之Logging[1]:Introduction

    在ASP.NET 4.X中,我们通常使用 log4net, NLog 等来记录日志,但是当我们引用的一些第三方类库使用不同的日志框架时,就比较混乱了.而在 ASP.Net Core 中内置了日志系统, ...

  6. ASP.NET Core 源码学习之 Options[4]:IOptionsMonitor

    前面我们讲到 IOptions 和 IOptionsSnapshot,他们两个最大的区别便是前者注册的是单例模式,后者注册的是 Scope 模式.而 IOptionsMonitor 则要求配置源必须是 ...

  7. ASP.NET Core 源码学习之 Options[3]:IOptionsSnapshot

    2017-06-28 更新: OptionsSnapshot 已改为 OptionsManager 变更详情 IOptionsCache 已改为 IOptionsMonitorCache 变更详情 在 ...

  8. ASP.NET Core 源码学习之 Options[2]:IOptions

    在 上一篇 中,介绍了一下Options的注册,而使用时只需要注入 IOption 即可: public ValuesController(IOptions<MyOptions> opti ...

  9. asp.net core源码飘香:Logging组件

    简介: 作为基础组件,日志组件被其他组件和中间件所使用,它提供了一个统一的编程模型,即不需要知道日志最终记录到哪里去,只需要调用它即可. 使用方法很简单,通过依赖注入ILogFactory(Creat ...

最新文章

  1. win7中Android开发环境搭建超详细(百度)
  2. (转)flash位图缓存cacheAsBitmap
  3. 离散对数(同余理论-BSGS算法)
  4. break stop java_在Java中使用break退出循环是不好的做法吗?
  5. CodeForces - 1341D Nastya and Scoreboard(dp+贪心)
  6. Android Studio 创建第一个Android工程项目
  7. SDOI 2009 BIll的挑战
  8. php远程simplexml_load_string()出错,PHP simplexml_load_string无法正常工作
  9. 利用正则按固定长度分割字符串
  10. 爆红的变老神器 FaceApp,夹杂着安全隐患?
  11. [Java] 蓝桥杯BASIC-28 基础练习 Huffuman树
  12. python基于协程的网络库gevent、eventlet
  13. 北京林业大学matlab公选课,【抛砖引玉】一学期结束分享一下部分公选课和通识课经验...
  14. Excel数据分析项目——电商数据分析实战
  15. MSP430的JTAG接口和BSW接口
  16. 手把手搭建VuePress文档
  17. mtex极图_一种多相金属材料相成分识别的方法与流程
  18. Android使用MediaRecorder的stop方法报stop failed错误的解决方案
  19. 量化开发必掌握的30个知识点【什么是分笔逐笔数据】?
  20. java lockmodetype_java-org.hibernate.lockmode.pessimistic_write的行为

热门文章

  1. 你与一份好简历之间的距离
  2. 狐狸文│区块链发展的正路
  3. jmeter(2)录制脚本
  4. NSInteger,NSUInteger,NSNumber
  5. 如何使用VIM的Help
  6. Linux的FHS(文件系统结构标准)剖析
  7. SQLServer支持大内存方案(转载)
  8. C# 日志管理框架:Common.Logging和log4net
  9. 开源的负载测试/压力测试工具 NBomber
  10. 面向.NET开发人员的Dapr——服务调用