如何让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 ...
随机推荐
- 多路复用IO:select poll epoll
[电话面试]io多路复用专题面试 这个真猛 有人做了笔记:点这里 select Select(Max+1,&rset,null,null,null)是因为0~max是max+1. 过程: 将文 ...
- 杂谈--User Story
本篇用于给自己后续慢慢看,对敏捷感兴趣的小伙伴,可以自行去看官方文档或者各种网站的视频讲解,更详细. 对于敏捷开发来说,User Story是开发的基础,把原本需求拆成最小粒度的Story,以方便拆分 ...
- day11-SpringBoot中注入Servlet&Filter&Listener
SpringBoot中注入Servlet&Filter&Listener 1.基本介绍 文档:SpringBoot中注入Servlet&Filter&Listener ...
- 解密Prompt系列4. 升级Instruction Tuning:Flan/T0/InstructGPT/TKInstruct
这一章我们聊聊指令微调,指令微调和前3章介绍的prompt有什么关系呢?哈哈只要你细品,你就会发现大家对prompt和instruction的定义存在些出入,部分认为instruction是promp ...
- mysql的concat与concat_ws拼接字符串的使用
concat的使用 可以拼接多个字符 mysql> select concat(name,dept,job) from t1; +-----------------------+ | conca ...
- 安装 Metrics server
安装 Metrics server Metrics Server 是 Kubernetes 内置自动缩放管道的可扩展.高效的容器资源指标来源. Metrics Server 从 Kubelets 收集 ...
- 自用纯C语言实现任务调度(可用于STM32、C51等单片机)
前言 这个任务调度模块的实现是形成于毕设项目中的,用在STM32中,断断续续跨度2个月实现了一些基本功能,可能后面再做其他项目时会一点点完善起来,也会多学习相关知识来强化模块的实用性和高效性,毕竟 ...
- 随机模块random os模块 序列化模块
random: 验证码的实现: choice是选择列表中任意一个 ##记得把randint取出来的数字转化成str类型,要不就会相加 ##cha()是把asc编码表里的数字转化成字符 更进一步做成函数 ...
- Java设计模式 —— 享元模式
14 享元模式 14.1 享元模式概述 Flyweight Pattern: 运用共享技术有效地支持大量细粒度对象的复用. 当系统中存在大量相同或相似的对象时,它通过共享技术实现相同或相似的细粒度对象 ...
- LeeCode 栈与队列问题(一)
LeeCode 20: 有效的括号 题目描述 给定一个只包括 '(', ')', '{', '}', '[', ']' 的字符串s,判断字符串是否有效. 有效字符串满足: 左括号必须用相同类型的右括号 ...