我没能实现始终在一个线程上运行 task
前文我们总结了在使用常驻任务实现常驻线程时,应该注意的事项。但是我们最终没有提到如何在处理对于带有异步代码的办法。本篇将接受笔者对于该内容的总结。
如何识别当前代码跑在什么线程上
一切开始之前,我们先来使用一种简单的方式来识别当前代码运行在哪种线程上。
最简单的方式就是打印当前线程名称和线程ID来识别。
private static void ShowCurrentThread(string work)
{
Console.WriteLine($"{work} - {Thread.CurrentThread.Name} - {Thread.CurrentThread.ManagedThreadId}");
}
通过这段代码,我们可以非常容易的识别三种不同情况下的线程信息。
[Test]
public void ShowThreadMessage()
{
new Thread(() => { ShowCurrentThread("Custom thread work"); })
{
IsBackground = true,
Name = "Custom thread"
}.Start();
Task.Run(() => { ShowCurrentThread("Task.Run work"); });
Task.Factory.StartNew(() => { ShowCurrentThread("Task.Factory.StartNew work"); },
TaskCreationOptions.LongRunning);
Thread.Sleep(TimeSpan.FromSeconds(1));
}
// output
// Task.Factory.StartNew work - .NET Long Running Task - 17
// Custom thread work - Custom thread - 16
// Task.Run work - .NET ThreadPool Worker - 12
分别为:
- 自定义线程 Custom thread
- 线程池线程 .NET ThreadPool Worker
- 由 Task.Factory.StartNew 创建的新线程 .NET Long Running Task
因此,结合我们之前昙花线程的例子,我们也可以非常简单的看出线程的切换情况:
[Test]
public void ShortThread()
{
new Thread(async () =>
{
ShowCurrentThread("before await");
await Task.Delay(TimeSpan.FromSeconds(0.5));
ShowCurrentThread("after await");
})
{
IsBackground = true,
Name = "Custom thread"
}.Start();
Thread.Sleep(TimeSpan.FromSeconds(1));
}
// output
// before await - Custom thread - 16
// after await - .NET ThreadPool Worker - 6
我们希望在同一个线程上运行 Task 代码
之前我们已经知道了,手动创建线程并控制线程的运行,可以确保自己的代码不会于线程池线程产生竞争,从而使得我们的常驻任务能够稳定的触发。
当时用于演示的错误示例是这样的:
[Test]
public void ThreadWaitTask()
{
new Thread(async () =>
{
ShowCurrentThread("before await");
Task.Run(() =>
{
ShowCurrentThread("inner task");
}).Wait();
ShowCurrentThread("after await");
})
{
IsBackground = true,
Name = "Custom thread"
}.Start();
Thread.Sleep(TimeSpan.FromSeconds(1));
}
// output
// before await - Custom thread - 16
// inner task - .NET ThreadPool Worker - 13
// after await - Custom thread - 16
这个示例可以明显的看出,中间的部分代码是运行在线程池的。这种做法会在线程池资源紧张的时候,导致我们的常驻任务无法触发。
因此,我们需要一种方式来确保我们的代码在同一个线程上运行。
那么接下来我们分析一些想法和效果。
加配!加配!加配!
我们已经知道了,实际上,常驻任务不能稳定触发是因为 Task 会在线程池中运行。那么增加线程池的容量自然就是最直接解决高峰的做法。 因此,如果条件允许的话,直接增加 CPU 核心数实际上是最为有效和简单的方式。
不过这种做法并不适用于一些类库的编写者。比如,你在编写日志类库,那么其实无法欲知用户所处的环境。并且正如大家所见,市面上几乎没有日志类库中由说明让用户只能在一定的 CPU 核心数下使用。
因此,如果您的常驻任务是在类库中,那么我们需要一种更为通用的方式来解决这个问题。
考虑使用同步重载
在 Task 出现之后,很多时候我们都会考虑使用异步重载的方法。这显然不是错误的做法,因为这可以使得我们的代码更加高效,提升系统的吞吐量。但是,如果你想要让 Thread 稳定的在同一个线程上运行,那么你需要考虑使用同步重载的方法。通过同步重载方法,我们的代码将不会出现线程切换到线程池的情况。自然也就实现了我们的目的。
总是使用 TaskCreationOptions.LongRunning
这个办法其实很不实际。因为任何一层没有指定,都会将任务切换到线程池中。
[Test]
public void AlwaysLogRunning()
{
new Thread(async () =>
{
ShowCurrentThread("before await");
Task.Factory.StartNew(() =>
{
ShowCurrentThread("LongRunning task");
Task.Run(() => { ShowCurrentThread("inner task"); }).Wait();
}, TaskCreationOptions.LongRunning).Wait();
ShowCurrentThread("after await");
})
{
IsBackground = true,
Name = "Custom thread"
}.Start();
Thread.Sleep(TimeSpan.FromSeconds(1));
}
// output
// before await - Custom thread - 16
// LongRunning task - .NET Long Running Task - 17
// inner task - .NET ThreadPool Worker - 7
// after await - Custom thread - 16
所以说,这个办法可以用。但其实很怪。
自定义 Scheduler
这是一种可行,但是非常困难的做法。虽然说自定义个简单的 Scheduler 也不是很难,只需要实现几个简单的方法。但要按照我们的需求来实现这个 Scheduler 并不简单。
比如我们尝试实现一个这样的 Scheduler:
注意:这个 Scheduler 并不能正常工作。
class MyScheduler : TaskScheduler
{
private readonly Thread _thread;
private readonly ConcurrentQueue<Task> _tasks = new();
public MyScheduler()
{
_thread = new Thread(() =>
{
while (true)
{
while (_tasks.TryDequeue(out var task))
{
TryExecuteTask(task);
}
}
})
{
IsBackground = true,
Name = "MyScheduler"
};
_thread.Start();
}
protected override IEnumerable<Task> GetScheduledTasks()
{
return _tasks;
}
protected override void QueueTask(Task task)
{
_tasks.Enqueue(task);
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return false;
}
}
上面的代码中,我们期待通过一个单一的线程来执行所有的任务。但实际上它反而是一个非常简单的死锁演示装置。
我们设想运行下面这段代码:
[Test]
public async Task TestLongRunningConfigureAwait()
{
var scheduler = new MyScheduler();
await Task.Factory.StartNew(() =>
{
ShowCurrentThread("BeforeWait");
Task.Factory
.StartNew(() =>
{
ShowCurrentThread("AfterWait");
}
, CancellationToken.None, TaskCreationOptions.None, scheduler)
.Wait();
ShowCurrentThread("AfterWait");
}, CancellationToken.None, TaskCreationOptions.None, scheduler);
}
这段代码中,我们期待,在一个 Task 中运行另外一个 Task。但实际上,这段代码会死锁。
因为,我们的 MyScheduler 中,我们在一个死循环中,不断的从队列中取出任务并执行。但是,我们的任务中,又会调用 Wait 方法。
我们不妨设想这个线程就是我们自己。
- 首先,老板交代给你一件任务,你把它放到队列中。
- 然后你开始执行这件任务,执行到一半发现,你需要等待第二件任务的执行结果。因此你在这里等着。
- 但是第二件任务这个时候也塞到了你的队列中。
- 这下好了,你手头的任务在等待你队列里面的任务完成。而你队列的任务只有你才能完成。
- 完美卡死。
因此,其实实际上我们需要在 Wait 的时候通知当前线程,此时线程被 Block 了,然后转而从队列中取出任务执行。在 Task 于 ThreadPool 的配合中,是存在这样的机制的。但是,我们自己实现的 MyScheduler 并不能与 Task 产生这种配合。因此需要考虑自定义一个 Task。跟进一步说,我们需要自定义 AsyncMethodBuilder 来实现全套的自定义。
显然者是一项相对高级内容,期待了解的读者,可以通过 UniTask1 项目来了解如何实现这样的全套自定义。
总结
如果你期望在常驻线程能够稳定的运行你的任务。那么:
- 加配,以避免线程池不够用
- 考虑在这部分代码中使用同步代码
- 可以学习自定义 Task 系统
参考
- .NET Task 揭秘(2):Task 的回调执行与 await2
- Task3
- TaskCreationOptions4
- 这样在 C# 使用 LongRunningTask 是错的5
- async 与 Thread 的错误结合6
- 实现常驻任务除了避免昙花线程,还需要避免重返线程池7
感谢阅读,如果觉得本文有用,不妨点击推荐或者在评论区留下 Mark,让更多的人可以看到。
欢迎关注作者的微信公众号“newbe技术专栏”,获取更多技术内容。
- 本文作者: newbe36524
- 本文链接: https://www.newbe.pro/Others/0x029-I-can-not-manage-to-always-run-task-on-one-thread/
- 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
https://github.com/Cysharp/UniTask
https://www.cnblogs.com/eventhorizon/p/15912383.html
https://threads.whuanle.cn/3.task/
https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions?view=net-7.0&WT.mc_id=DX-MVP-5003606
https://www.newbe.pro/Others/0x026-This-is-the-wrong-way-to-use-LongRunnigTask-in-csharp/
https://www.newbe.pro/Others/0x027-error-when-using-async-with-thread/
https://www.newbe.pro/Others/0x028-avoid-return-to-threadpool-in-longrunning-task
我没能实现始终在一个线程上运行 task的更多相关文章
- ios8.1上运行程序,程序界面只显示一部分
在ios 9.1上运行程序没问题 但是在8.1上运行发现模拟器上只显示了程序的一小部分界面,没有显示完全. 结果发现由以下代码设置问题引起的 - (BOOL)application:(UIApplic ...
- C#中的线程(上)-入门 分类: C# 线程 2015-03-09 10:56 53人阅读 评论(0) 收藏
1. 概述与概念 C#支持通过多线程并行地执行代码,一个线程有它独立的执行路径,能够与其它的线程同时地运行.一个C#程序开始于一个单线程,这个单线程是被CLR和操作系统(也称为"主线 ...
- 全球在一个 level 上思考的价值观和想法是一样的(转)
近日,福布斯中文版总编辑周建工对话马云,谈到腾讯频繁的大笔收购,马云点评称腾讯收购的所有的案子,老百性都看得懂,这就错了.战略就像买股票一样,如果老太太都开始买股票了,一定有问题. 以下是对话内容,转 ...
- Python 之并发编程之线程上
一.线程概念 进程是资源分配的最小单位 线程是计算机中调度的最小单位 多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都 ...
- 如何在一个页面上让多个jQuery
如何在一个页面上让多个jQuery共存呢?比如jquery-1.5和jquery-1.11. 你可能会问,为什么需要在一个页面上让多个jQuery共存?直接引用最新版本的jQuery不行吗? 答案是, ...
- ZeroMQ接口函数之 :zmq_send – 在一个socket上发送一个消息帧
ZeroMQ 官方地址 :http://api.zeromq.org/4-1:zmq-send zmq_send(3) ØMQ Manual - ØMQ/4.1.0 Name ...
- 通过浏览器https能够访问SVN,但eclipse SVN,tortoiseSVN始终连接不上SVN的问题解决方案
为了便于本地代码维护,特意在本地搭建了一个visualSVN服务器用于本地代码管理,但是最近突然出现问题,eclipse上的SVN资源库始终连接不上,提示 "svn: connection ...
- FOR ALL ENTRIES IN 与 INNER JOIN 写在一个SQL上影响效率
SELECT likp~vbeln likp~lfart lips~werks likp~kunnr INTO CORRESPONDING FIELDS OF TABLE it_likps FROM ...
- 在没装VS2010的机器上运行VS2010开发的C++程序
在VS2010下写了一个win32控制台应用程序,编译ok.exe,需要依赖osg相关动态库 第一次编译的是Debug版本的,直接将ok.exe和osg相关dll文件拷贝到没有安装VS2010机器上运 ...
- “不支持一个STA线程上针对多个句柄的WaitAll。”的解决方案
一.异常提示 不支持一个 STA 线程上针对多个句柄的 WaitAll. 出错界面如下图: 二.解决方法 先直接上解决方案吧.其实解决方法很简单如下面的代码直接把main函数的[STAThread]属 ...
随机推荐
- Communications link failure:The last packet successfully received from the server was 0 millisecond ago
出现这种错误的大致情况如下: 1.数据库连接长时间未使用,断开连接后,再去连接出现这种情况.这种情况常见于用连接池连接数据库出现的问题 2.数据库连接的后缀参数问题 针对上述两种情况,解决方案如下 1 ...
- wpf 使用了 template 了的 combobox 中,displaymemberpath 有 bug,仅在 popup 中生效
需求是:仅想改变combobox的默认样式,所以 template 是直接在属性窗口点击转化为本地值的. using System.Collections.Generic; using System. ...
- 优先使用C++的别名声明(using)来替换typedef
C++98中,我们如果想用简写的方式表达一个类型,那么可以使用typedef关键字: typedef std::unique_ptr<std::unordered_map<std::str ...
- 深入理解Linux系统调用
1.系统调用号查询 我的学号位数是08,在64位调用表里可以查到对应的系统调用函数是__x64_sys_lseek 2.lseek函数 由于没用过该函数,所以先去了解一下这个函数的作用: 直白的说就是 ...
- Mysql5.7的安装与卸载与数据迁移
Mysql5.7的安装与卸载 安装: 1.安装的时候 只选择安装 server即可 2.安装过程中,一定要重新选择安装路径和各个日志的路径,将来会非常大,不适合存在系统盘下面 3.安装时,取消MySQ ...
- CV入坑
https://www.cnblogs.com/fldev/p/14360149.html
- C Ⅸ
数组例子:统计个数 #include <stdio.h> int main(void) { int x; int count[10]; int i; ...
- TCP长连接管道通信改消息队列涉及到的知识点
1.队列,链表,STL 1.C++ STL标准库简介 长久以来,软件界一直希望建立一种可重复利用的东西,以及一种得以制造出"可重复运用的东西" 的方法,从函数(functions) ...
- sql几种不同的权限
DDL:Data Definition Language DDL允许用户定义数据,也就是创建表.删除表.修改表结构这些操作.通常,DDL由数据库管理员执行. DML:Data Manipulation ...
- freopen函数
C/C++ 输入输出到文件 freopen("in.txt","r",stdin); freopen("out.txt","w&q ...
