如何让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 ...
随机推荐
- vite项目生产环境去掉console信息【转载】
环境变量引入 通常去掉console为生产环境,即需要引入环境变量.具体请看这篇文章: vite项目初始化之~环境变量 注意 与webpacak相比,vite已经将这个功能内置到了,所以我们只需要配置 ...
- 玩转Mybatis高级特性:让你的数据操作更上一层楼
目录 动态SQL 缓存机制 插件机制 自定义类型转换 总结 Mybatis高级特性能够帮助我们更加灵活地操作数据库,包括动态SQL.缓存机制.插件机制.自定义类型转换等.学习这些特性可以让我们更好地利 ...
- 内核不中断前提下,Gaussdb(DWS)内存报错排查方法
摘要:本文主要讲解如何在内核保证操作不能中断采取的特殊处理,理论上用户执行的sql使用的内存(dynamic_used_memory) 是不会大范围的超过max_dynamic_memory的内存的 ...
- Windows注册表中修改UAC(用户账号控制)及批处理脚
我在windows电脑上安装软件时,或者设置开机启动软件时,会先弹出提示框: 用户账户控制 你要允许此应用对你的设备进行更改吗? 很烦人,我不想每次都提醒.需要关闭"用户账户控制" ...
- pandas之loc/iloc操作
在数据分析过程中,很多时候需要从数据表中提取出相应的数据,而这么做的前提是需要先"索引"出这一部分数据.虽然通过 Python 提供的索引操作符"[]"和属性操 ...
- SpringBoot入门(二):Controller的使用
Controller中注解的使用: @Controller ●该注解用来响应页面,必须配合模板来使用 @RestController ●该注解可以直接响应字符串,返回的类型为JSON格式 ...
- mysql 清空数据表id 重1开始 帝国cms清空数据表id 重1开始
alter table phome_ecms_news auto_increment=1; alter table phome_ecms_news_check auto_increment=1; al ...
- 通过命令快速找到python的路径
查询Python 首先我们需要知道Python安装路径,可以在命令行中逐行执行下面代码 python3 import sys sys.executable
- 【vue3-element-admin 】基于 Vue3 + Vite4 + TypeScript + Element-Plus 从0到1搭建后台管理系统(前后端开源@有来开源组织)
vue3-element-admin 是基于 vue-element-admin 升级的 Vue3 + Element Plus 版本的后台管理前端解决方案,技术栈为 Vue3 + Vite4 + T ...
- 苞米豆的多数据源 → dynamic-datasource-spring-boot-starter,挺香的!
开心一刻 2023年元旦,我妈又开始了对我的念叨 妈:你到底想多少岁结婚 我:60 妈:60,你想找个多大的 我:找个55的啊,她55我60,结婚都有退休金,不用上班不用生孩子,不用买车买房,成天就是 ...