在我们 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)线程。

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

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

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

  2. asp.net core源码飘香:从Hosting开始

    知识点: 1.Kestrel服务器启动并处理Http请求的过程. 2.Startup的作用. 源码飘香: 总结: asp.net core将web开发拆分为多个独立的组件,大多以http中间件的形式添 ...

  3. ASP.NET Core应用的错误处理[1]:三种呈现错误页面的方式

    由于ASP.NET Core应用是一个同时处理多个请求的服务器应用,所以在处理某个请求过程中抛出的异常并不会导致整个应用的终止.出于安全方面的考量,为了避免敏感信息的外泄,客户端在默认的情况下并不会得 ...

  4. 一张图理清ASP.NET Core启动流程

    1. 引言 对于ASP.NET Core应用程序来说,我们要记住非常重要的一点是:其本质上是一个独立的控制台应用,它并不是必需在IIS内部托管且并不需要IIS来启动运行(而这正是ASP.NET Cor ...

  5. 全面理解 ASP.NET Core 依赖注入

    DI在.NET Core里面被提到了一个非常重要的位置, 这篇文章主要再给大家普及一下关于依赖注入的概念,身边有工作六七年的同事还个东西搞不清楚.另外再介绍一下.NET  Core的DI实现以及对实例 ...

  6. Asp.net Core Startup Class中是如何获取配置信息的

    默认的网站构建方式 VS2015新建asp.net core项目,项目建立完成后,有两个文件,Program.cs和Startup.cs. public class Program { public ...

  7. ASP.NET Core URL Rewrite中间件

    URL重写是基于一个或多个预置规则修改请求URL的行为.URL重写在资源位置和访问地址之间创建了一种抽象,这样二者之间就减少了紧密的联系.URL重写有多种适用的场景: 临时或永久移动或替换服务器资源, ...

  8. Asp.Net Core 入门(一)——Program.cs做了什么

    ASP.NET Core 是微软推出的一种全新的跨平台开源 .NET 框架,用于在 Windows.Mac 或 Linux 上生成基于云的新式 Web 应用程序.国内目前关于Asp.Net Core的 ...

  9. ASP.NET Core 的 Dependency Injection

    ASP.NET Core使用了大量的DI(Dependency Injection)设计,有用过Autofac或类似的DI Framework对此应该不陌生.本篇将介绍ASP.NET Core的依赖注 ...

随机推荐

  1. python的线程和进程

    1.线程的基本概念 概念 线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程 ...

  2. PYthon3:函数实现“自动售卖机”功能

    题目: 自动贩卖机: # 只接受1元.5元.10元的纸币或硬币,可以1块,5元,10元.# 饮料只有橙汁.椰汁.矿泉水.早餐奶,售价分别是3.5,4,2,4.5# 写一个函数用来表示贩卖机的功能:用户 ...

  3. 数位dp 的简单入门

    时间紧张,就不讲那么详细了. 之前一直被深搜代码误解,以为数位dp 其实就是记忆化深搜...(虽说爆搜确实很舒服而且还好想) 但是后来发现数位dp 的标准格式其实是 预处理 + dp ...... 数 ...

  4. VMware的NAT网络模式

    参考链接:https://www.cnblogs.com/linjiaxin/p/6476480.html 图例:

  5. sublim 插件

    sublim 插件 https://www.cnblogs.com/hykun/p/sublimeText3.html html 代码自动 + tab ul>li>img+p+a ! ul ...

  6. Java面试题复习笔记(数据库)

    1.数据库分类? 关系型数据库和非关系型. 常用关系型:Myspl.Oracle.SQLServer 非关系型:Redis.Hadoop.Memcache.Mogobd 2.关系数据库三范式? 范式就 ...

  7. LMerge-github

    ILMerge ILMerge是一个将多个.NET程序集合并到一个程序集中的实用程序.它可以免费使用,并以NuGet包的形式提供. 如果您在使用它时遇到任何问题,请与我们联系.(mbarnett at ...

  8. 如何把PDF文件拆分为多个文件

    一个PDF文件有很多个PDF页面组成,有时候我们只需要单个页面的时候应该怎么做呢,这个时候就需要拆分PDF文件了,那么如何把 PDF文件拆分为多个文件呢,应该有很多的小伙伴都想知道吧,那就让我们一起来 ...

  9. sql语句的删除

    SQL中delete * from 和 delete from 有什么区别? 在SQL Server中两者没有区别,但在Oracle和MySQL的SQL语句中,delete * from是不标准的语法 ...

  10. Java Web 中使用ffmpeg实现视频转码、视频截图

    Java Web 中使用ffmpeg实现视频转码.视频截图 转载自:[ http://www.cnblogs.com/dennisit/archive/2013/02/16/2913287.html  ...