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

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


如何使用 Task.Run?

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

接下来分析原因:

示例程序和示例代码

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

可以发现,虽然我们是同一时间启动的 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

参考资料

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

  1. 【SpringCloud】HystrixCommand的threadPoolKey默认值及线程池初始化

    关于threadPoolKey默认值的疑问 使用SpingCloud必然会用到Hystrix做熔断降级,也必然会用到@HystrixCommand注解,@HystrixCommand注解可以配置的除了 ...

  2. 线程池ThreadPool及Task调度死锁分析

    近1年,偶尔发生应用系统启动时某些操作超时的问题,特别在使用4核心Surface以后.笔记本和台式机比较少遇到,服务器则基本上没有遇到过. 这些年,我写的应用都有一个习惯,就是启动时异步做很多准备工作 ...

  3. 【C# 线程】线程池 ThreadPool

    Overview    如今的应用程序越来越复杂,我们常常需要使用<异步编程:线程概述及使用>中提到的多线程技术来提高应用程序的响应速度.这时我们频繁的创建和销毁线程来让应用程序快速响应操 ...

  4. 线程池ThreadPool的初探

    一.线程池的适用范围 在日常使用多线程开发的时候,一般都构造一个Thread示例,然后调用Start使之执行.如果一个线程它大部分时间花费在等待某个事件响应的发生然后才予以响应:或者如果在一定期间内重 ...

  5. 高效线程池(threadpool)的实现

    高效线程池(threadpool)的实现 Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们.在网络编程中,一般都是基于Reactor线 ...

  6. 多线程Thread,线程池ThreadPool

    首先我们先增加一个公用方法DoSomethingLong(string name),这个方法下面的举例中都有可能用到 #region Private Method /// <summary> ...

  7. 线程池ThreadPool的常用方法介绍

    线程池ThreadPool的常用方法介绍 如果您理解了线程池目的及优点后,让我们温故下线程池的常用的几个方法: 1. public static Boolean QueueUserWorkItem(W ...

  8. Python之路(第四十六篇)多种方法实现python线程池(threadpool模块\multiprocessing.dummy模块\concurrent.futures模块)

    一.线程池 很久(python2.6)之前python没有官方的线程池模块,只有第三方的threadpool模块, 之后再python2.6加入了multiprocessing.dummy 作为可以使 ...

  9. 线程池ThreadPool实战

    线程池ThreadPool 线程池概念 常用线程池和方法 1.测试线程类 2.newFixedThreadPool固定线程池 3.newSingleThreadExecutor单线程池 4.newCa ...

随机推荐

  1. JavaScript算法相关

    1. 不使用循环,创建一个长度为100的数组,并且每个元素的值等于它的下标? Array.apply(null, {length: N}).map(Function.call, Number); Ar ...

  2. C# Random循环生成随机数重复问题解决方案

    C# Random循环生成随机数重复问题解决方案1.当我们通过Random生成随机数时,习惯的写法如下: int a=new Random().Next(0,100); 然后生成一个数据数没有任何问题 ...

  3. 把Java Web工程转换为基于Maven的Web工程

    有一个之前的工程,在使用了基于Maven的Web开发后,发现这种方式很便利,于是就想把之前老的传统的J2EE Web Project转为Maven Web Project. 转换的思路如下: 1.新建 ...

  4. LeetCode 48. Rotate Image My Submissions Question (矩阵旋转)

    题目大意:给一个矩阵,将其按顺时针旋转90°. 题目分析:通法是先将矩阵转置,然后再反转每一行,或者是先反转每一列,然后再将其转置.I just want to say"It's amazi ...

  5. redis 处理命令的过程

    redis版本:redis-3.2.9 在客户端输入 set name zhang,调试redis服务器,得到调用栈如下: 在dictReplace中加了断点,结果跳出来4个线程,redis还是单进程 ...

  6. 微信小程序--登录流程梳理

    前言 微信小程序凡是需要记录用户信息都需要登录,但是也有几种不同的登录方式,但是在小程序部分的登录流程是一样的.之前就朦朦胧胧地用之前项目的逻辑改改直接用了,这个新项目要用就又结合官方文档重新梳理了下 ...

  7. PHP:第三章——PHP中函数的实参多余形参的处理方法

    <?phpheader("Content-Type:text/html;charset=utf-8");//传参的函数/*function F($a){    echo $a ...

  8. delete symlink in subversion using svn delete command

    # svn delete etc/systemd/system/getty.target.wants/serial-getty@ttyS3.service@ D         etc/systemd ...

  9. pyqt5 使用 QTimer, QThread, pyqtSignal 实现自动执行,多线程,自定义信号触发。

    渣渣用法,请等待我心情好的时候更新. 1.第一个例子 1.1 先看mainwindow.py from PyQt5 import QtCore, QtGui, QtWidgets class Ui_M ...

  10. Win7下的flutter环境安装配置

    随着 2018 年底 GOOGLE 正式发布了 flutter1.0,这个原生开发框架大火,试用了一下确实不错,代码状态即时刷新,所见即所得.APP 开发的环境安装,比较复杂,很多初学者在这一步就被 ...