.NET 中小心嵌套等待的 Task,它可能会耗尽你线程池的现有资源,出现类似死锁的情况
一个简单的 Task 不会消耗多少时间,但如果你不合适地将 Task 转为同步等待,那么也可能很快耗尽线程池的所有资源,出现类似死锁的情况。
本文将以一个最简单的例子说明如何出现以及避免这样的问题。
本文内容
耗时的 Task.Run
谁都不会认为 Task.Run(() => 1) 这个异步任务执行会消耗多少时间。
但实际上,如果你的代码写得不清真,它真的能消耗大量的时间,这种时间消耗有点像死锁。
下图分别是 7 个这样的任务、8 个这样的任务和 16 个这样的任务的耗时:

可以发现,8 个任务和 16 个任务的耗时很不正常。
在实际的测试当中,1~7 个任务的耗时几乎相同,而到后面每增加一个任务会增加大量时间。
| 任务个数 | 耗时 (ms) |
|---|---|
| 1 | 39 |
| 2 | 54 |
| 3 | 58 |
| 4 | 50 |
| 5 | 49 |
| 6 | 45 |
| 7 | 54 |
| 8 | 1027 |
| 9 | 2030 |
| 10 | 3027 |
| 11 | 4027 |
| 12 | 5032 |
| 13 | 6027 |
| 14 | 7029 |
| 15 | 8025 |
| 16 | 9025 |
任务计时采用的是 Stopwatch,关于为什么要使用这种计时方式,可以阅读 .NET/C# 在代码中测量代码执行耗时的建议(比较系统性能计数器和系统时间)

从图中,我们可以很直观地观察到,每多一个任务,就会多花 1 秒的事件。这可以认为默认情况下线程池在增加线程的时候,发现如果线程不够,会等待 1 秒之后才会创建新的线程。
最简复现代码
class Program
{
static async Task Main(string[] args)
{
Console.Title = "walterlv task demo";
var stopwatch = Stopwatch.StartNew();
var task = Enumerable.Range(0, 8).Select(i => Task.Run(() => DoAsync(i).Result)).ToList();
await Task.WhenAll(task);
Console.WriteLine($"耗时: {stopwatch.Elapsed}");
Console.Read();
}
private static async Task<int> DoAsync(int index)
{
return await Task.Run(() => 1);
}
}
原因
你可以阅读 .NET 默认的 TaskScheduler 和线程池(ThreadPool)设置 了解线程池创建新工作线程的规则。这里其实真的是类似于死锁的一个例子。
- 一开始,我们创建了 n 个 Task,然后分别安排在线程池中执行,并在每个 Task 中等待任务执行完毕;
- 随后这 n 个 Task 分别再创建了 n 个子 Task,并继续安排在线程池中执行;
- 这时问题来了,由于前面 n 个 Task 在等待中,所以占用了线程池的线程资源:
- 如果 n < 线程池最小线程数,那么当前线程池中还有剩余工作线程帮助完成子 Task;
- 但如果 n >= 线程池最小线程数,那么当前线程池中便没有新的工作线程来完成子 Task;于是一开始的等待也不会完成;必须等线程池开启新的工作线程后,任务才可以继续。
带线程池开启新的线程之前,以上那些线程就是处于死锁的状态!由于线程池开启新的工作线程需要等待一段时间(例如每秒最多开启一个新的线程),所以每增加一个这样的任务,那么消耗的时间便会持续增加。
解决
去掉这里本来多余的 Task.Run 问题便可以解决。或者一直 async/await 中间不要转换为同步代码,那么问题也能解决。
我会遇到以上代码,是因为在库中写了类似 DoAsync 那样的方法。同时为了方便使用,封装了一个同步等待的属性。在业务使用方,觉得获取此属性可能比较耗时,于是用了 Task.Run 在后台线程调用。同时由于这是一个可能大量并发的操作,于是造成了以上悲剧。
更多死锁问题
死锁问题:
- 使用 Task.Wait()?立刻死锁(deadlock) - walterlv
- 不要使用 Dispatcher.Invoke,因为它可能在你的延迟初始化 Lazy 中导致死锁 - walterlv
- 在有 UI 线程参与的同步锁(如 AutoResetEvent)内部使用 await 可能导致死锁
- .NET 中小心嵌套等待的 Task,它可能会耗尽你线程池的现有资源,出现类似死锁的情况 - walterlv
解决方法:
- 在编写异步方法时,使用 ConfigureAwait(false) 避免使用者死锁 - walterlv
- 将 async/await 异步代码转换为安全的不会死锁的同步代码(使用 PushFrame) - walterlv
.NET 中小心嵌套等待的 Task,它可能会耗尽你线程池的现有资源,出现类似死锁的情况的更多相关文章
- Spring Boot中使用@Async的时候,千万别忘了线程池的配置!
上一篇我们介绍了如何使用@Async注解来创建异步任务,我可以用这种方法来实现一些并发操作,以加速任务的执行效率.但是,如果只是如前文那样直接简单的创建来使用,可能还是会碰到一些问题.存在有什么问题呢 ...
- ThreadPoolExecutor中策略的选择与工作队列的选择(java线程池)
工作原理 1.线程池刚创建时,里面没有一个线程.任务队列是作为参数传进来的.不过,就算队列里面有任务,线程池也不会马上执行它们. 2.当调用 execute() 方法添加一个任务时,线程池会做如下判断 ...
- C#线程篇---Task(任务)和线程池不得不说的秘密(5)
在上篇最后一个例子之后,我们发现了怎么去使用线程池,调用ThreadPool的QueueUserWorkItem方法来发起一次异步的.计算限制的操作,例子很简单,不是吗? 然而,在今天这篇博客中,我们 ...
- C#线程篇---Task(任务)和线程池不得不说的秘密
我们要知道的是,QueueUserWorkItem这个技术存在许多限制.其中最大的问题是没有一个内建的机制让你知道操作在什么时候完成,也没有一个机制在操作完成是获得一个返回值,这些问题使得我们都不敢启 ...
- linux环境中关闭tomcat,通过shutdown.sh无法彻底关闭--线程池
最近测试环境上测试的项目通过shutdown.sh始终无法彻底关闭. 之前临时解决方法两种: 第一:通过ps -ef|grep tomcat查看到tomcat的进程直接使用kill来杀死进程. 第二: ...
- 死锁、Lock锁、等待唤醒机制、线程组、线程池、定时器、单例设计模式_DAY24
1:线程(理解) (1)死锁 概念: 同步中,多个线程使用多把锁之间存在等待的现象. 原因分析: a.线程1将锁1锁住,线程2将锁2锁住,而线程1要继续执行锁2中的代码,线程2要继续执行锁1中的代码, ...
- 二 Java利用等待/通知机制实现一个线程池
接着上一篇博客的 一Java线程的等待/通知模型 ,没有看过的建议先看一下.下面我们用等待通知机制来实现一个线程池 线程的任务就以打印一行文本来模拟耗时的任务.主要代码如下: 1 定义一个任务的接口 ...
- Java 中的几种线程池,你之前用对了吗
好久不发文章了,难道是因为忙,其实是因为懒.这是一篇关于线程池使用和基本原理的科普水文,如果你经常用到线程池,不知道你的用法标准不标准,是否有隐藏的 OOM 风险.不经常用线程池的同学,还有对几种线程 ...
- 线程池ThreadPool及Task调度死锁分析
近1年,偶尔发生应用系统启动时某些操作超时的问题,特别在使用4核心Surface以后.笔记本和台式机比较少遇到,服务器则基本上没有遇到过. 这些年,我写的应用都有一个习惯,就是启动时异步做很多准备工作 ...
随机推荐
- [Java学习] 再谈Java包
在Java中,为了组织代码的方便,可以将功能相似的类放到一个文件夹内,这个文件夹,就叫做包. 包不但可以包含类,还可以包含接口和其他的包. 目录以"\"来表示层级关系,例如 E:\ ...
- 49 DOM(2)
一.value属性: input ,select 标签 ,textarea 标签中有value属性, 获取他们属性值的方法,先获取该元素ele,然后ele.value得到value值. <!DO ...
- python-day8-列表的内置方法
# l=[1,2,3] #l=list([1,2,3])# print(type(l)) #pat1===>优先掌握部分# 索引## 切片l=['a','b','c','d','e','f'] ...
- 计时(.NET)
using System.Diagnostics; // 开始计时 Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); // 要计时的操 ...
- 简话Angular 02 Angular控制器-作用域嵌套
一句话: 就是孩子可以啃老,老子不能动孩子一根毛! * 子控制器有父控制器里变量的所有权限,可以读取,也可以修改. * 父控制器不能读,也不能修改孩子的变量 1. html代码 <div ng- ...
- JdbcTemplate.queryForObject
} catch (EmptyResultDataAccessException e) { log.info(">>>检测到有无记录>>>>" ...
- POJ 2263 最短路Floyd_warshall算法
灰常开心的用Floyd变形写出来了.额.对米来说还是牺牲了一定的脑细胞的.然而.我发现.大牛们还可以神奇的用Kruskal求最大生成树的最小权值来写.也可以用Dijkatra变形来写.T_T....5 ...
- java 继承 初始化顺序
面向对象三大特性: 封装,继承,多态 java 继承初始化顺序 先初始化父类对象, 在父类对象中先初始化父类属性,再初始化父类的构造方法,然后初始化子类对象,初始化子类对象的属性,初始化子类的构造方法 ...
- Swift网络封装库Moya中文手册之Endpoints
Endpoints Endpoint是一种半私有的数据结构,Moya用来解释网络请求的根本构成.一个endpoint储存了以下数据: The URL. The HTTP method (GET,POS ...
- LinkedStack<T>
public class LinkedStack<T> { private static class Node<U> { U item; Node<U> next; ...