百度统计
一面之猿网
让这个世界,因为我,有一点点的不一样
纯序员给你介绍图化框架的简单实现——线程池优化(六)

大家好,我是不会写代码的纯序员——Chunel Feng,好久没写文章了。前阵子,自己工作有一些异动,也换了方向。一个月适应下来,整体感觉还好。这个,在最后会跟大家聊两句。

这篇文章,主要跟大家聊一聊近期对线程池功能的优化。在之前的文章中,我们已经从设计、优化和结果几个方面,聊了对UThreadPool类 相关的内容。今天的内容,算是在原有的基础上,做一些简单的追加。

首先,还是先上源码:CGraph
github链接 源码

image-1664896422623

超时机制

首先,我们来聊聊线程执行超时的问题。

我们在实际工作中,遇到跑线程任务的时候,总有些不可预知的情况(比如,数据库慢查询),导致个别任务很慢,而上游一直在等待返回结果。甚至有可能,多个慢的任务,导致上游功能层的流程完全阻塞了。

为了避免这种情况,我们需要对单个线程的执行时间,做一个时间限定,比如:当前任务不能超过 3000ms,如果超过,要结束阻塞,并且返回错误信息。别小看这个功能,在有些情况下,这是可以帮上大忙的。

image-1664896686368

用起来也很简单,看下面一段代码。在调用的时候,需要在后面加入 超时时间信息,单位是ms。

auto tp = new UThreadPool();
CStatus status = tp->submit([] { sleep(2500); }, 2000);
status += tp->submit([] { sleep(2500); }, 3000);    // 会超时,结果提现在status的结果中

其实,思路也挺简单的,用的是 std::future<> wait_for(单个)wait_until(多个)的方法进行等待。如果超过约定时长,则停止等待,并且返回错误信息。

需要强调的一点是,上游的线程执行函数返回,并且继续进行接下来的操作了。但是,线程函数仍然会继续执行,并且占用相关资源,直到该线程函数执行完毕,或者本进程结束。

也就是说,切记不要再 while(true) 的线程中,设定等待时长,然后就不管了。这样的话,程序虽然还是正常运行,但是线程资源是释放不了的。

任务组

接着,我们来聊聊任务组(UTaskGroup)的事情。我们知道,线程池一般都用来处理批量任务。在前面的内容中,我们也都是通过for循环的方式,将一堆任务放到线程池中执行。考虑下面几个问题:

  • 我想等这一批任务执行结束,再执行其他的任务,怎么办?
  • 我想给这一批任务,设定一个统一的等待时长,怎么办?
  • 我想在多批任务执行结束的时候,固定执行某个回调逻辑,怎么办?

给大家推荐任务组。任务组的设计,主要就是为了方便对多任务的管理。使得多个任务,表现出一些统一的特性(比如,这一批任务最多执行 5秒),也方便后期的复用和移植。一组任务,最重要的就是整整齐齐。

image

UTaskGroup taskGroup;
auto tp = new UThreadPool();
for (int i = 0; i < 100; i++) {
    taskGroup.addTask([i] { std::cout << i << " "; });    // 将任务放到一个taskGroup中,并发执行。执行的结果是无序的
}
tp->submit(taskGroup, 2000);    // 最多执行 2000ms,就结束

任务组还可以设定结束时候的回调,多个UTaskGroup复用一个结束逻辑(如:记录超时次数)。关于使用方法,大家看一个源码中的例子,肯定也就明白了,不多说了。

优先级和阻塞执行

最后,我们来聊聊任务优先级的问题。在实际开发中,我们经常会遇到这样的问题,多任务之间,基本是没有什么依赖和关联关系的,但是多少会有优先级会有些区别。比如,m个键盘输入任务和n个打印任务,明显前者应该更优先于后者。

为此,我们专门设定了根据优先级执行的逻辑。上游可以将任务绑定优先级,并且写入线程池中。线程池会在内部进行统一排序,并且最终执行。

void priority(UThreadPoolPtr tp) {
    tp->commitWithPriority([] { print_func(); }, -3);    // 优先级设定为-3
    tp->commitWithPriority([] { keyboard_func_1(); }, 10);    // 优先级设定为10
    tp->commitWithPriority([] { keyboard_func_2(); }, 10);
    tp->commitWithPriority([] { keyboard_func_3(); }, 10);
    tp->commitWithPriority([] { commen_func(); }, 0);
}

需要说明几点:

首先,这种优先级任务的执行,是不依赖主线程中的消息队列,而仅依赖辅助线程执行的。如果“仅需要”执行这种优先级任务,建议在配置中将主线程个数设置为0。

static const int CGRAPH_DEFAULT_THREAD_SIZE = 0;
static const int CGRAPH_MAX_THREAD_SIZE = 2;

还有就是,这种机制有一个好处,可以优化线程资源配置。假设这种场景:有很多长时间执行的任务,一股脑的塞入线程池中,将所有的主线程和辅助线程全部占满了。那继续push进来的任务,就都无法正常执行了。久而久之,甚至可能导致oom和进程崩溃。

其实,有个假设,就是一般情况下,需要较长时间运行的线程,都不是很care是否能够立即开始:比如,你敲下键盘(短时间任务),就希望屏幕上立刻给出反馈;而你想打印一份很长的文档(长时间任务),点下按钮后,打印机过个5秒钟再开始工作,你就不会有感觉。

针对这种情况,我们将部分长时间执行的任务设定一个较低的优先级,放到pool内部的优先队列中,仅让辅助线程来执行它们。这样,既能保证这些任务在合适的时间被执行,又能保证不会占用主线程资源,更耗尽所有线程资源。

本章小结

这一章的内容,主要是针对最近一些朋友提出来的意见和建议,做的一些改进。说的都是一些很小很实用的点,也伴随着大量的改动点、优化点和测试点。

在这里,我们感谢来自 qtcn社区 的大佬,给我们提出的意见和解决方案。如果有遇到在Windows上将线程池封装成dll后,无法正常运行的问题,请参考:线程池封装问题和解决方法,也欢迎大家加入qtcn社区。我刚开始工作的时候,是有机会接触一下qt的。当时没学好,现在整个就很遗憾。

还要感谢杭州凤起路旁的金主爸爸,给我们一面之猿网提供的了新的服务器。我开一面之猿网两年多,赚的钱还不够买服务器的,悲剧啊。小破站持续接收广告中,包括且不限于 茶叶、西柚、奶茶,我们底线很低的。希望大家多投广告,也增加打赏的力度,让我有点资本,再去狗币圈里疯狂收割一波。

image

最后,如果你看到这里,说明也是对这一块内容比较感兴趣的朋友了。所在团队近期(2022.10.04)持续招人,组内技术氛围好,工程力强,在我混进来之前,人均大佬。如果你对 索引、存储、调度、异构计算 等一个或多个技术领域有自己的理解和研究,并且熟悉数据结构和操作系统的话,希望可以跟您取得联系,期待与您一起共事的机会。

mmqrcode1602771241876

                                                   [2022.10.04 by Chunel]

推荐阅读


个人信息

微信: ChunelFeng
邮箱: chunel@foxmail.com
个人网站:www.chunel.cn
github地址: https://github.com/ChunelFeng

image