如何让Task在非线程池线程中执行?
Task承载的操作需要被调度才能被执行,由于.NET默认采用基于线程池的调度器,所以Task默认在线程池线程中执行。但是有的操作并不适合使用线程池,比如我们在一个ASP.NET Core应用中承载了一些需要长时间执行的后台操作,由于线程池被用来处理HTTP请求,如果这些后台操作也使用线程池来调度,就会造成相互影响。在这种情况下,使用独立的一个或者多个线程来执行这些后台操作可能是一个更好的选择。
一、基于线程池的调度
二、TaskCreationOptions.LongRunning
三、换成异步操作呢?
四、换种写法呢?
五、调用Wait方法
六、自定义TaskScheduler
七、独立线程池
一、基于线程池的调度
我们通过如下这个简单的程序来验证默认基于线程池的Task调度。我们调用Task类型的静态属性Factory返回一个TaskFactory对象,并调用其StartNew方法启动一个Task对象,这个Task指向的Run方法会在一个循环中调用Do方法。Do方法使用自选等待的方式模拟一段耗时2秒的操作,并在控制台输出当前线程的IsThreadPoolThread属性确定是否是线程池线程。
Task.Factory.StartNew(Run);
Console.Read(); void Run()
{
while (true)
{
Do();
}
} void Do()
{
var end = DateTime.UtcNow.AddSeconds(2);
SpinWait.SpinUntil(() => DateTimeOffset.UtcNow > end);
var isThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread;
Console.WriteLine($"[{DateTimeOffset.Now}]Is thread pool thread: {isThreadPoolThread}");
}
通过如下所示的输出结果,我们得到了答案:利用TaskFactory创建的Task在默认情况下确实是通过线程池的形式被调度的。

二、TaskCreationOptions.LongRunning
很明显,上述Run方法是一个需要永久执行的LongRunning操作,并不适合使用线程池来执行,实际上TaskFactory在设计的时候就考虑到了这一点,我们利用它创建一个Task的时候可以指定对应的TaskCreationOptions选项,其中一个选项就是LongRuning。我们通过如下的方式修改了上面这段程序,在调用StartNew方法时指定了这个选项。
Task.Factory.StartNew(Run, TaskCreationOptions.LongRunning);
Console.Read(); void Run()
{
while (true)
{
Do();
}
} void Do()
{
var end = DateTime.UtcNow.AddSeconds(2);
SpinWait.SpinUntil(() => DateTimeOffset.UtcNow > end);
var isThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread;
Console.WriteLine($"[{DateTimeOffset.Now}]Is thread pool thread: {isThreadPoolThread}");
}
再次执行我们的程序就会通过如下的输出结果看到Do方法将不会在线程池线程中执行了。

三、换成异步操作呢?
由于LongRunning操作经常会涉及IO操作,所以我们执行方法经常会写成异步的形式。如下所示的代码中,我们将Do方法替换成DoAsync,将2秒的自旋等待替换成Task.Delay。由于DoAsync写成了异步的形式,Run也换成对应的RunAsync。
Task.Factory.StartNew(RunAsync, TaskCreationOptions.LongRunning);
Console.Read(); async Task RunAsync()
{
while (true)
{
await DoAsync();
}
} async Task DoAsync()
{
await Task.Delay(2000);
var isThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread;
Console.WriteLine($"[{DateTimeOffset.Now}]Is thread pool thread: {isThreadPoolThread}");
}
再次启动程序后,我们发现又切换成了线程池调度了。为什么会这样呢?其实很好理解,由于原来返回void的Run方法被替换成了返回Task的RunAsync,传入StartNew方法表示执行操作的委托类型从Action切换成了Func<Task>,虽然我们指定了LongRunning选项,但是StartNew方法只是采用这种模式执行Func<Task>这个委托对象而已,而这个委托在遇到await的时候就返回了。终于返回的Task对象,还是按照默认的方式进行调度执行。

四、换种写法呢?
有人说,上面我们使用的是一个方法来表示作为参数的委托对象,如果我们按照如下的方式使用基于async/await的Lambda表达式呢?实际上这样的Lambda表达式就是Func<Task>的另一种编写方式而已。
Task.Factory.StartNew(async () => { while (true) await DoAsync();}, TaskCreationOptions.LongRunning);
Console.Read();
async Task DoAsync()
{
await Task.Delay(2000);
var isThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread;
Console.WriteLine($"[{DateTimeOffset.Now}]Is thread pool thread: {isThreadPoolThread}");
}
五、调用Wait方法
其实这个问题很好解决,按照如下的方式将DoAsync方法换成同步形式的Do,将基于await的等待替换成针对Wait方法的调用就可以了。我想当你接触Task的时候,就有很多人不断提醒你,谨慎使用Wait方法,因为它会阻塞当前线程。实际上对于我们的硬要用场景,调用Wait方法才是正确的选择,因为我们的初衷就是使用一个独立的线程以独占的方式来执行所需的操作。
Task.Factory.StartNew(() => { while (true) Do(); }, TaskCreationOptions.LongRunning);
Console.Read();
void Do()
{
Task.Delay(2000).Wait();
var isThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread;
Console.WriteLine($"[{DateTimeOffset.Now}]Is thread pool thread: {isThreadPoolThread}");
}
六、自定义TaskScheduler
既然针对线程池的使用是“Task调度”导致的,那么我们自然可以通过重写TaskScheduler的方式来解决这个问题。如下这个自定义的DedicatedThreadTaskScheduler 会采用独立的线程来执行被调度的Task,线程的数量可以参数来指定。
internal sealed class DedicatedThreadTaskScheduler : TaskScheduler
{
private readonly BlockingCollection<Task> _tasks = new();
private readonly Thread[] _threads;
protected override IEnumerable<Task>? GetScheduledTasks() => _tasks;
protected override void QueueTask(Task task) => _tasks.Add(task);
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) => false;
public DedicatedThreadTaskScheduler(int threadCount)
{
_threads = new Thread[threadCount];
for (int index = 0; index < threadCount; index++)
{
_threads[index] = new Thread(_ =>
{
while (true)
{
TryExecuteTask(_tasks.Take());
}
});
}
Array.ForEach(_threads, it => it.Start());
}
}
我们演示实例中Run/Do方法再次还原成如下所示的纯异步模式的RunAsync/DoAsync,并在调用StartNew方法的时候创建一个DedicatedThreadTaskScheduler对象作为最后一个参数。
Task.Factory.StartNew(RunAsync, CancellationToken.None, TaskCreationOptions.LongRunning, new DedicatedThreadTaskScheduler(1));
Console.Read(); async Task RunAsync()
{
while (true)
{
await DoAsync();
}
} async Task DoAsync()
{
await Task.Delay(2000);
var isThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread;
Console.WriteLine($"[{DateTimeOffset.Now}]Is thread pool thread: {isThreadPoolThread}");
}
由于创建的Task将会使用指定的DedicatedThreadTaskScheduler 对象来调度,DoAsync方法自然就不会在线程池线程中执行了。

七、独立线程池
.NET提供的线程池是一个全局共享的线程池,而我们定义的DedicatedThreadTaskScheduler相当于创建了一个独立的线程池,对象池的效果可以通过如下这个简单的程序展现出来。
Task.Factory.StartNew(()=> Task.WhenAll( Enumerable.Range(1,6).Select(it=>DoAsync(it))),
CancellationToken.None,
TaskCreationOptions.None,
new DedicatedThreadTaskScheduler(2)); async Task DoAsync(int index)
{
await Task.Yield();
Console.WriteLine($"[{DateTimeOffset.Now.ToString("hh:MM:ss")}]Task {index} is executed in thread {Environment.CurrentManagedThreadId}");
var endTime = DateTime.UtcNow.AddSeconds(4);
SpinWait.SpinUntil(() => DateTime.UtcNow > endTime);
await Task.Delay(1000);
}
Console.ReadLine();
如上面的代码片段所示,异步方法DoAsync利用自旋等待模拟了一段耗时4秒的操作,通过调用Task.Delay方法模拟了一段耗时1秒的IO操作。我们在其中输出了任务开始执行的时间和当前线程ID。在调用的StartNew方法中,我们调用这个DoAsync方法创建了6个Task,这些Task交给创建的DedicatedThreadTaskScheduler进行调度。我们为这个DedicatedThreadTaskScheduler指定的线程数量为2。从如下所示的输出结果可以看出,6个操作确实在两个线程中执行的。

如何让Task在非线程池线程中执行?的更多相关文章
- C# 显式创建线程 or 使用线程池线程--new Thread() or ThreadPool.QueueUserWorkItem()
在C#多线程编程中,关于是使用自己创建的线程(Thread)还是使用线程池(ThreadPool)线程,一直很困惑,知道看了Jeffrey Richter的相关介绍才明白,记录如下: 当满足一下任何条 ...
- java 线程池线程忙碌且阻塞队列也满了时给一个拒接的详细报告
线程池线程忙碌且阻塞队列也满了时给一个拒接的详细报告.下面是一个自定义的终止策略类,继承了ThreadPoolExecutor.AbortPolicy类并覆盖了rejectedExecution方法把 ...
- EventStore .NET API Client在使用线程池线程同步写入Event导致EventStore连接中断的问题研究
最近,在使用EventStore的.NET Client API采用大量线程池线程同步写入Event时(用于模拟ASP.NET服务端大并发写入Event的情况),发现EventStore的连接会随机中 ...
- Java多线程面试题:线程锁+线程池+线程同步等
1.并发编程三要素? 1)原子性 原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行. 2)可见性 可见性指多个线程操作一个共享变量时,其中一个线程对变量 ...
- TestNg线程池配置、执行次数配置、超时配置
使用注解的方式对TestNg线程池配置.执行次数配置.超时配置 注:使用注解来控制测试方法运行的次数和超时时间,timeOut在单线程或者多线程模式下都可用,threadPoolSize设置了线程池的 ...
- 浅谈线程池(中):独立线程池的作用及IO线程池
原文地址:http://blog.zhaojie.me/2009/07/thread-pool-2-dedicate-pool-and-io-pool.html 在上一篇文章中,我们简单讨论了线程池的 ...
- android线程与线程池-----线程池(二)《android开发艺术与探索》
android 中的线程池 线程池的优点: 1 重用线程池中的线程,避免了线程的创建和销毁带来的性能开销 2 能有效的控制最大并发数,避免大量线程之间因为喜欢抢资源而导致阻塞 3 能够对线程进行简单的 ...
- 线程池线程数与(CPU密集型任务和I/O密集型任务)的关系
近期看了一些JVM和并发编程的专栏,结合自身理解,来做一个关于(线程池线程数与(CPU密集型任务和I/O密集型任务)的关系)的总结: 1.任务类型举例: 1.1: CPU密集型: 例如,一般我们系统的 ...
- 通过transmittable-thread-local源码理解线程池线程本地变量传递的原理
前提 最近一两个月花了很大的功夫做UCloud服务和中间件迁移到阿里云的工作,没什么空闲时间撸文.想起很早之前写过ThreadLocal的源码分析相关文章,里面提到了ThreadLocal存在一个不能 ...
- 001-多线程-JUC线程池-线程池架构-Executor、ExecutorService、ThreadPoolExecutor、Executors
一.概述 1.1.线程池架构图 1. Executor 它是"执行者"接口,它是来执行任务的.准确的说,Executor提供了execute()接口来执行已提交的 Runnable ...
随机推荐
- 思必驰周强:AI 和传统信号技术在实时音频通话中的应用
如何用 AI 解决声音传输&处理中的三大问题?三大问题又是哪三大问题? 在「RTE2022 实时互联网大会」中,思必驰研发总监 @周强以<AI 和传统信号技术在实时音频通话中的应用> ...
- adb命令启动报错Error: unknown command '-start'怎么办
大家好,每天记录小问题.水滴石穿. 今天介绍一个从0开始启动app应用的app命令 adb shell am -start -w -n 包名/启动名 第一次运行时报错 怎么办呢, 这边使用的是雷电模拟 ...
- 从零开始学习 Java 系列之你为什么要学 Java?
全文大约[4000]字,不说废话,只讲可以让你学到技术.明白原理的纯干货! 在正式开始本系列教程之前,壹哥希望先用一篇文章,来扫清你学习前的认知障碍.请坚定自己的学习信念,不要半途而废浪费时间,壹哥希 ...
- 设计模式(二十九)----综合应用-自定义Spring框架-Spring IOC相关接口分析
1 BeanFactory解析 Spring中Bean的创建是典型的工厂模式,这一系列的Bean工厂,即IoC容器,为开发者管理对象之间的依赖关系提供了很多便利和基础服务,在Spring中有许多IoC ...
- Object.toString与Object.prototype.toString区别
1.Object原型链上的toString方法可以用于对象类型的判断,如常用的区分数组与普通对象. 例如: Object.prototype.toString.call(''); //[object ...
- Defi开发简介
Defi开发简介 介绍 Defi是去中心化金融的缩写, 是一项旨在利用区块链技术和智能合约创建更加开放,可访问和透明的金融体系的运动. 这与传统金融形成鲜明对比,传统金融通常由少数大型银行和金融机构控 ...
- window启动和停止服务命令
NET STOP serviceNET STOP 用于终止 Windows 服务.终止一个服务可以取消这个服务正在使用的任何一个网络连接.同样的一些服务是依赖于另外一些服务的.终止一个服务就会终止其它 ...
- python入门教程之二环境搭建
环境搭建 1python解释器 当我们编写Python代码时,我们得到的是一个包含Python代码的以.py为扩展名的文本文件.要运行代码,就需要Python解释器去执行.py文件. 由于整个Pyth ...
- ChatGPT4实现前一天
目录 提出需求 代码实现 需求分析 单元测试 等价类划分 决策表 软件测试作业,用ChatGPT4来帮个小忙,小划水,勿喷勿喷,近期有相关作业的同学看到我的文章,建议修改一下,别撞车了,哈哈哈~ 提出 ...
- LeeCode 二叉树问题(四)
二叉搜索树的应用问题 二叉搜索树的定义 若左子树不空,则左子树上所有节点的值均小于根节点的值 若右子树不空,则右子树上所有节点的值均大于根节点的值 它的左右子树也均为二叉搜索树 中序遍历结果为一个升序 ...