一码阻塞,万码等待:ASP.NET Core 同步方法调用异步方法“死锁”的真相
在我们 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 同步方法调用异步方法“死锁”的真相的更多相关文章
- Dapr 运用之集成 Asp.Net Core Grpc 调用篇
前置条件: <Dapr 运用> 改造 ProductService 以提供 gRPC 服务 从 NuGet 或程序包管理控制台安装 gRPC 服务必须的包 Grpc.AspNetCore ...
- asp.net core源码飘香:从Hosting开始
知识点: 1.Kestrel服务器启动并处理Http请求的过程. 2.Startup的作用. 源码飘香: 总结: asp.net core将web开发拆分为多个独立的组件,大多以http中间件的形式添 ...
- ASP.NET Core应用的错误处理[1]:三种呈现错误页面的方式
由于ASP.NET Core应用是一个同时处理多个请求的服务器应用,所以在处理某个请求过程中抛出的异常并不会导致整个应用的终止.出于安全方面的考量,为了避免敏感信息的外泄,客户端在默认的情况下并不会得 ...
- 一张图理清ASP.NET Core启动流程
1. 引言 对于ASP.NET Core应用程序来说,我们要记住非常重要的一点是:其本质上是一个独立的控制台应用,它并不是必需在IIS内部托管且并不需要IIS来启动运行(而这正是ASP.NET Cor ...
- 全面理解 ASP.NET Core 依赖注入
DI在.NET Core里面被提到了一个非常重要的位置, 这篇文章主要再给大家普及一下关于依赖注入的概念,身边有工作六七年的同事还个东西搞不清楚.另外再介绍一下.NET Core的DI实现以及对实例 ...
- Asp.net Core Startup Class中是如何获取配置信息的
默认的网站构建方式 VS2015新建asp.net core项目,项目建立完成后,有两个文件,Program.cs和Startup.cs. public class Program { public ...
- ASP.NET Core URL Rewrite中间件
URL重写是基于一个或多个预置规则修改请求URL的行为.URL重写在资源位置和访问地址之间创建了一种抽象,这样二者之间就减少了紧密的联系.URL重写有多种适用的场景: 临时或永久移动或替换服务器资源, ...
- Asp.Net Core 入门(一)——Program.cs做了什么
ASP.NET Core 是微软推出的一种全新的跨平台开源 .NET 框架,用于在 Windows.Mac 或 Linux 上生成基于云的新式 Web 应用程序.国内目前关于Asp.Net Core的 ...
- ASP.NET Core 的 Dependency Injection
ASP.NET Core使用了大量的DI(Dependency Injection)设计,有用过Autofac或类似的DI Framework对此应该不陌生.本篇将介绍ASP.NET Core的依赖注 ...
随机推荐
- C# Textbox 自动换行
方法1 使用textbox的AppendText方法 方法2 textBox.ScrollToCaret(); this.textBox.Focus();//获取焦点 this.textBox.Sel ...
- ado.net 使用:ExecuteReader 无法获取输出参数
解决方法: 要获取到输出参数.需要连接关闭之后才行. 一般都是用using把打开数据库连接的reader包起来
- JAVA进阶10
间歇性混吃等死,持续性踌躇满志系列-------------第10天 1.Random package cn.intcast.day08.demo01; import java.util.Random ...
- Fiddler模拟自动响应数据
Fiddler模拟自动响应数据 定位到要修改的部分 2.将返回的数据保存到本地,保存成网页,并修改响应数据 找到修改的部分,修改之 3.再次请求刷新首页,将工具定位到autoresponder将接口加 ...
- 配置 Docker 加速器:适用于 Ubuntu14.04、Debian、CentOS6 、CentOS7、Fedora、Arch Linux、openSUSE Leap 42.1
天下容器, 唯快不破 Docker Hub 提供众多镜像,你可以从中自由下载数十万计的免费应用镜像, 这些镜像作为 docker 生态圈的基石,是我们使用和学习 docker 不可或缺的资源.为了解决 ...
- 01.pandas
01.Series # -*- coding: utf-8 -*- """ Series 객체 특징 - pandas 제공 1차원 자료구성 - DataFrame 칼 ...
- SQL反模式学习笔记9 元数据分裂
目标:支持可扩展性.优化数据库的结构来提升查询的性能以及支持表的平滑扩展. 反模式:克隆表与克隆列 1.将一张很长的表拆分成多张较小的表,使用表中某一个特定的数据字段来给这些拆分出来的表命名. 2.将 ...
- contos最小包安装完后一些准备
yum upgradeyum install net-toolsyum -y install wgetyum -y install vim-enhanced yum install gcc gcc-c ...
- 习题集1b: 额外练习 (可选)
1.练习:4.样本特点 用来描述样本的数字叫做? □ 参数 (√)□ 统计量 □ 变量 □ 常数 2.练习:5.大一学生体重情况 Freidman 博士在一所大学任教,她记录了所在大学每位大一新生 ...
- React(v16.8.4)生命周期详解
当前版本v16.8.4 装载过程(组件第一次在DOM树中渲染的过程): constructor(常用) -> getInitialState(v16.0已废弃) -> getDefault ...