当前位置: 首页 > news >正文

.NET 中小心嵌套等待的 Task,它可能会耗尽你线程池的现有资源,出现类似死锁的情况

一个简单的 Task 不会消耗多少时间,但如果你不合适地将 Task 转为同步等待,那么也可能很快耗尽线程池的所有资源,出现类似死锁的情况。

本文将以一个最简单的例子说明如何出现以及避免这样的问题。


本文内容

      • 耗时的 Task.Run
      • 最简复现代码
      • 原因
      • 解决
      • 更多死锁问题

耗时的 Task.Run

谁都不会认为 Task.Run(() => 1) 这个异步任务执行会消耗多少时间。

但实际上,如果你的代码写得不清真,它真的能消耗大量的时间,这种时间消耗有点像死锁。

下图分别是 7 个这样的任务、8 个这样的任务和 16 个这样的任务的耗时:

简单异步任务的耗时

可以发现,8 个任务和 16 个任务的耗时很不正常。

在实际的测试当中,1~7 个任务的耗时几乎相同,而到后面每增加一个任务会增加大量时间。

任务个数耗时 (ms)
139
254
358
450
549
645
754
81027
92030
103027
114027
125032
136027
147029
158025
169025

任务计时采用的是 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)设置 了解线程池创建新工作线程的规则。这里其实真的是类似于死锁的一个例子。

  1. 一开始,我们创建了 n 个 Task,然后分别安排在线程池中执行,并在每个 Task 中等待任务执行完毕;
  2. 随后这 n 个 Task 分别再创建了 n 个子 Task,并继续安排在线程池中执行;
  3. 这时问题来了,由于前面 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

相关文章:

  • 在有 UI 线程参与的同步锁(如 AutoResetEvent)内部使用 await 可能导致死锁
  • 不要使用 Dispatcher.Invoke,因为它可能在你的延迟初始化 LazyT 中导致死锁
  • 定义一组抽象的 Awaiter 的实现接口,你下次写自己的 await 可等待对象时将更加方便
  • .NET 除了用 Task 之外,如何自己写一个可以 await 的对象?
  • .NET 中什么样的类是可使用 await 异步等待的?
  • Visual Studio 2017 以前的旧格式的 csproj Import 进来的 targets 文件有时不能正确计算属性(PropertyGroup)和集合(ItemGroup)
  • 使用 ReSharper,输入即遵循 StyleCop 的代码格式化规范
  • StyleCop 是什么,可以帮助团队带来什么价值?
  • 文件和文件夹不存在的时候,FileSystemWatcher 监听不到文件的改变?如果递归地监听就可以了
  • C#/.NET 使用 CommandLineParser 来标准化地解析命令行
  • .NET 中使用 TaskCompletionSource 作为线程同步互斥或异步操作的事件
  • 使用 WPF 开发一个 Windows 屏幕保护程序
  • 在 Windows 10 中开启移动 WLAN 热点
  • .NET/C# 项目如何优雅地设置条件编译符号?
  • 在 Roslyn 分析语法树时添加条件编译符号的支持
  • 《网管员必读——网络组建》(第2版)电子课件下载
  • 〔开发系列〕一次关于小程序开发的深度总结
  • 10个确保微服务与容器安全的最佳实践
  • extjs4学习之配置
  • input实现文字超出省略号功能
  • Lsb图片隐写
  • 爱情 北京女病人
  • 测试开发系类之接口自动化测试
  • 码农张的Bug人生 - 见面之礼
  • 使用 Xcode 的 Target 区分开发和生产环境
  • 思维导图—你不知道的JavaScript中卷
  • 这几个编码小技巧将令你 PHP 代码更加简洁
  • 3月27日云栖精选夜读 | 从 “城市大脑”实践,瞭望未来城市源起 ...
  • ​猴子吃桃问题:每天都吃了前一天剩下的一半多一个。
  • ### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTr
  • $.type 怎么精确判断对象类型的 --(源码学习2)
  • (12)Hive调优——count distinct去重优化
  • (9)目标检测_SSD的原理
  • (C#)获取字符编码的类
  • (C语言)输入一个序列,判断是否为奇偶交叉数
  • (LeetCode) T14. Longest Common Prefix
  • (附源码)php投票系统 毕业设计 121500
  • (机器学习的矩阵)(向量、矩阵与多元线性回归)
  • (一)UDP基本编程步骤
  • (转)利用PHP的debug_backtrace函数,实现PHP文件权限管理、动态加载 【反射】...
  • (轉貼) VS2005 快捷键 (初級) (.NET) (Visual Studio)
  • (轉貼)《OOD启思录》:61条面向对象设计的经验原则 (OO)
  • (最优化理论与方法)第二章最优化所需基础知识-第三节:重要凸集举例
  • .NET Core 控制台程序读 appsettings.json 、注依赖、配日志、设 IOptions
  • .net core使用ef 6
  • .net 发送邮件
  • .NET 分布式技术比较
  • .NET 线程 Thread 进程 Process、线程池 pool、Invoke、begininvoke、异步回调
  • .NET 中小心嵌套等待的 Task,它可能会耗尽你线程池的现有资源,出现类似死锁的情况
  • .NET委托:一个关于C#的睡前故事
  • /usr/bin/perl:bad interpreter:No such file or directory 的解决办法
  • ??eclipse的安装配置问题!??
  • @JsonFormat与@DateTimeFormat注解的使用
  • [ C++ ] STL---仿函数与priority_queue
  • [ vulhub漏洞复现篇 ] Celery <4.0 Redis未授权访问+Pickle反序列化利用