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

了解 .NET 的默认 TaskScheduler 和线程池(ThreadPool)设置,避免让 Task.Run 的性能急剧降低

.NET Framework 4.5 开始引入 Task.Run,它可以很方便的帮助我们使用 async / await 语法,同时还使用线程池来帮助我们管理线程。以至于我们编写异步代码可以像编写同步代码一样方便。

不过,如果滥用,也可能导致应用的性能急剧下降。本文将说明在默认线程池配置(ThreadPoolTaskScheduler)的情况下,应该如何使用 Task.Run 来避免性能的急剧降低。


本文内容

      • 如何使用 Task.Run?
      • 示例程序和示例代码
      • TaskScheduler
      • ThreadPool
      • 推荐的使用方法
        • 参考资料

如何使用 Task.Run?

  1. 对于 IO 操作,尽量使用原生提供的 Async 方法(不要自己使用 Task.Run 调用一个同步的版本占用线程池资源);
  2. 对于没有 Async 版本的 IO 操作,如果可能耗时很长,则指定 CreateOptionsLongRunning
  3. 其他短时间执行的任务才推荐使用 Task.Run

接下来分析原因:

示例程序和示例代码

在开始之前,我们先准备一个测试程序。这个程序一开始就使用 Task.Run 跑起来 10 个异步任务,每一个里面都等待 5 秒。

使用 Task.Run 跑起来的异步代码

可以发现,虽然我们是同一时间启动的 10 个异步任务,但任务的实际开始时间并不相同 —— 前面 8 个任务立刻开始了,而后面每隔一秒才会启动一个新的异步任务。

示例程序的代码如下:

class Program
{
    static async Task Main(string[] args)
    {
        Console.Title = "walterlv task demo";

        var task = Enumerable.Range(0, 10).Select(i => Task.Run(() => LongTimeTask(i))).ToList();
        await Task.WhenAll(task);

        Console.Read();
    }

    private static void LongTimeTask(int index)
    {
        var threadId = Thread.CurrentThread.ManagedThreadId.ToString().PadLeft(2, ' ');
        var line = index.ToString().PadLeft(2, ' ');
        Console.WriteLine($"[{line}] [{threadId}] [{DateTime.Now:ss.fff}] 异步任务已开始……");

        // 这一句才是关键,等待。其他代码只是为了输出。
        Thread.Sleep(5000);

        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine($"[{line}] [{threadId}] [{DateTime.Now:ss.fff}] 异步任务已结束……");
        Console.ForegroundColor = ConsoleColor.White;
    }
}

TaskScheduler

造成以上异步任务不马上开始的原因,与 Task 使用的 TaskScheduler 有关。默认情况下,Task.Run 使用的是 .NET 提供的默认 Scheduler,可以通过 TaskScheduler.Default 获取到。

Task 使用 TaskScheduler 来决定何时执行一个异步任务,如果你不设置,默认的实现是 ThreadPoolTaskScheduler

你可以前往 .NET Core 的源码页面查看源码:ThreadPoolTaskScheduler.QueueTask。

于是,你在线程池中的设置将决定一个 Task 将在何时开启一个线程执行。

ThreadPool

通过 ThreadPool.GetMinThreads 可以获得最小的线程数和异步 IO 完成线程数;通过 ThreadPool.GetMaxThreads 来获得其最大值。通过对应的 set 方法来设置最小值和最大值。

在 ThreadPool.GetMinThreads(Int32, Int32) Method (System.Threading) - Microsoft Docs

  • 线程池按需提供新的工作线程或 I/O 完成线程直到它达到每个类别的最小值。
  • 默认情况下,最小线程数设置为在系统上的处理器数。
  • 当达到最小值时,线程池可以创建该类别中的其他线程或等待,直到一些任务完成。
  • 需求较低时,线程池线程的实际数量可以低于最小值。

于是便会出现我们在本文一开始运行时出现的结果图。在我的计算机上(八核),最小线程数是 8,于是开始的 8 个任务可以立即开始执行。当达到数量 8 而依然没有线程完成执行的时候,线程池会尝试等待任务完成。但是,1 秒后依然没有任务完成,于是线程池创建了一个新的线程来执行新的任务;接下来是每隔一秒会开启一个新的线程来执行现有任务。当有任务完成之后,就可以直接使用之前完成了任务的线程继续完成新的任务。

不过,每个类别创建线程的总数量受到最大线程数限制。

推荐的使用方法

了解到 ThreadPoolTaskScheduler 的默认行为之后,我们可以做这些事情来充分利用线程池带来的优势:

  1. 对于 IO 操作,尽量使用原生提供的 Async 方法,这些方法使用的是 IO 完成端口,占用线程池中的 IO 线程而不是普通线程(不要自己使用 Task.Run 占用线程池资源);
  2. 对于没有 Async 版本的 IO 操作,如果可能耗时很长,则指定 CreateOptionsLongRunning(这样便会直接开一个新线程,而不是使用线程池)。
  3. 其他短时间执行的任务才推荐使用 Task.Run

参考资料

  • TaskScheduler Class (System.Threading.Tasks) - Microsoft Docs
  • TaskCreationOptions Enum (System.Threading.Tasks) - Microsoft Docs
  • Parallel Tasks - Microsoft Docs
  • Attached and Detached Child Tasks - Microsoft Docs
  • 在 ThreadPool.GetMinThreads(Int32, Int32) Method (System.Threading) - Microsoft Docs
  • Managed Threading Best Practices - Microsoft Docs

相关文章:

  • .NET 中小心嵌套等待的 Task,它可能会耗尽你线程池的现有资源,出现类似死锁的情况
  • 在有 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# 项目如何优雅地设置条件编译符号?
  • es6--symbol
  • git 常用命令
  • Java方法详解
  • mysql常用命令汇总
  • mysql外键的使用
  • Netty源码解析1-Buffer
  • python学习笔记-类对象的信息
  • Spark VS Hadoop:两大大数据分析系统深度解读
  • swift基础之_对象 实例方法 对象方法。
  • 程序员该如何有效的找工作?
  • 短视频宝贝=慢?阿里巴巴工程师这样秒开短视频
  • #13 yum、编译安装与sed命令的使用
  • #162 (Div. 2)
  • (12)Hive调优——count distinct去重优化
  • (2)STL算法之元素计数
  • (附源码)计算机毕业设计SSM疫情社区管理系统
  • (南京观海微电子)——COF介绍
  • (十七)devops持续集成开发——使用jenkins流水线pipeline方式发布一个微服务项目
  • (完整代码)R语言中利用SVM-RFE机器学习算法筛选关键因子
  • (一)kafka实战——kafka源码编译启动
  • (一)搭建springboot+vue前后端分离项目--前端vue搭建
  • (转)可以带来幸福的一本书
  • ****** 二 ******、软设笔记【数据结构】-KMP算法、树、二叉树
  • .NET Core 版本不支持的问题
  • .net framework profiles /.net framework 配置
  • .NET建议使用的大小写命名原则
  • .NET框架类在ASP.NET中的使用(2) ——QA
  • .NET上SQLite的连接
  • .NET业务框架的构建
  • :O)修改linux硬件时间
  • [ 云计算 | AWS ] AI 编程助手新势力 Amazon CodeWhisperer:优势功能及实用技巧
  • [ 云计算 | Azure 实践 ] 在 Azure 门户中创建 VM 虚拟机并进行验证
  • [AIGC] MySQL存储引擎详解
  • [BZOJ 4034][HAOI2015]T2 [树链剖分]
  • [C#]winform部署yolov5-onnx模型
  • [C++][基础]1_变量、常量和基本类型
  • [CentOs7]图形界面
  • [Docker]十一.Docker Swarm集群raft算法,Docker Swarm Web管理工具
  • [halcon案例2] 足球场的提取和射影变换
  • [IE编程] IE中对网页进行截图的编程接口