当前位置: 首页 > article >正文

C++ 新特性实现 ThreadPool

序言

 在之前我们实现过线程池,但是非常基础。答题思路就是实现一个安全的队列,再通过 ThreadPool 来管理队列和线程,对外提供一个接口放入需要执行的函数,但是这个函数是无参无返回值的。
 参数的问题我们可以使用 bind 来封装,但是函数返回值的问题需要我们解决。


一、为什么需要线程池?

 假如你在运行一个应用程序,其中主线程可能正在运行重要的程序逻辑和更新 UI。但是现在有一个消耗较大的任务需要执行,比如加载一个文件。如果你的主线程去执行该任务,那么应用程序的界面就会出现卡住的情况。这对于用户来说不是一个友好的使用体验,这时候就可以创建一个子线程去执行该加载任务,避免占用主线程。
 在程序执行的过程中可能会有大量的其他的任务需要我们去执行,这时候就可以使用子线程去执行该任务。但是每次执行的时候我们都需要创建一个线程,当任务密集的时候,线程创建和销毁的开销就会急剧上升,线程池因此而生。
 线程池的主要逻辑是预先创建一批线程,当任务到达时将任务放入队列中,线程池中的线程从队列中获取任务并执行。这种设计确保了线程的复用,从而减少了频繁创建和销毁线程的开销,如下所示:
在这里插入图片描述

二、简单线程池的实现

2.1 模块分析

 一个线程池的模块最重要的可以分为三部分:

  • 执行的任务:一个需要执行的任务
  • 任务队列:存放需要执行的任务
  • 线程池:管理任务队列和线程,对外提供增加任务的函数

2.2 模块实现

 首先是初始化函数

explicit ThreadPool(int numThreads = MAXTHREADNUM): _threadNum(numThreads), _threads(_threadNum), _running(false)
{}

我们创建了对应大小的线程数组来管理线程,并且初始时,线程池没有开启工作状态。

 之后是启动函数,也是线程开始工作的函数:

// 线程入口函数
void threadEntrance() {while (true) {std::unique_lock<std::mutex> lck(_mtx);// 执行条件_cond.wait(lck, [this]{ return !_queue.empty() || !_running; });// 避免泄露任务if (!_running && _queue.empty()) {return;}// 取出任务执行Task tsk = std::move(_queue.front());_queue.pop();lck.unlock();tsk();}
}void Start() {// 开始运行_running = true;for (int i = 0; i < _threadNum; i++) {// 每一个线程执行入口函数_threads[i] = std::thread(&ThreadPool::threadEntrance, this);}   
}

创建指定数量的线程,并且为每一个线程指定入口函数,入口函数的逻辑大体是:

  • 询问执行任务,取出任务执行
  • 不存在执行任务,如果是运行态阻塞,非运行态退出

我们来看一下我认为是较为关键的代码逻辑:

 // 执行条件_cond.wait(lck, [this]{ return !_queue.empty() || !_running; });// 避免泄露任务if (!_running && _queue.empty()) {return;}

不太冷的冷知识💡:|| 操作符在左边为 true 时直接返回,只有左边为 false 时,才会判断右边的真假

只有队列为空的时候我们才会判断 !_running 的真假:

  • true:往后执行
  • false:继续等待任务队列有新的任务后唤醒

之后还需要判断避免任务队列为空,线程才退出。这样做的目的是 — 避免任务队列不为空退出,这样会造成任务的泄漏。

 之后是较为简单的添加任务函数:

// 添加任务
void addTask(Task task) {if (!_running) {// 不合理的请求,抛出异常处理throw std::runtime_error("Its not start.");}// 添加任务std::unique_lock<std::mutex> lck(_mtx);_queue.push(std::move(task));lck.unlock();_cond.notify_all();
}

保证线程安全即可。

 最后是析构函数:

void stop() {// 停止执行_running = false;_cond.notify_all();
}~ThreadPool() {stop();// 回收线程for (std::thread &th : _threads) {th.join();}
}

在正式回收线程之前,我们需要通知其他线程,停止了,大家收工了。这里需要 _cond.notify_all(); 的原因是以防线程都因为队列为空在等待的情况。

2.3 不足之处

 这正如标题所言,这是一个最简单的实现方式,存在以下问题:

  • 用户需要传递一个无参的函数,有参的话可以使用 std::bind
  • 用户需要传递一个无返回的函数,这个怎么实现呢?

我们需要一个更为全面的线程池来解决上述两个问题。第一个问题我们可以使用可变参数来让用户传递参数,如:

void add(int x, int y) {int z = x + yreturn;
}int x = 1;
int y = 2;
// 这样就方便多了
pool.addTask(add, x, y);

第二个问题我们需要使用到异步编程的函数,接下来就会介绍到。


三、异步编程 — std::packaged_task

3.1 功能

 用于封装一个可调用对象,并与一个 future 关联。通过 packaged_task,可以异步执行某个任务,并在任务完成时通过 future 获取结果。

3.2 示例

 没有什么比得上一个示例更为直观的了:

int add(int x, int y) {std::this_thread::sleep_for(std::chrono::seconds(3));return x + y;
}int main() {// 将一个可调用对象封装为 packaged_taskstd::packaged_task<int(int, int)> tsk(add);// 获取与之相对应的 futurestd::future res = tsk.get_future();// 执行该任务int x = 1, y = 2;std::thread th(tsk, x, y);printf("I am waiting!\n");int result = res.get();printf("The result is %d\n", result);th.join();return 0;
}

这里需要注意,当我们的异步获取的结果未准备时,在主线程获取会被阻塞住。经过使用,我们也可以发现这个封装了的函数和普通的区别是:可以传递返回值
 这个功能还挺实用的,后面可以探索一下底层怎么实现。


四、Plus 版线程池

 我们需要修改的唯一地方是 addTask 函数,现在我先展示最后的结果:

/*
使用模板:可以传递任意类型的函数类型
可变参数:传递任意数量的参数
*/ 
template<class Func, class... Args>
auto addTask(Func &&f, Args&&... args)-> std::future<decltype(f(args...))> {if (!_running) {// 不合理的请求,抛出异常处理throw std::runtime_error("ThreadPool is not running.");}// 推断返回类型using returnType = decltype(f(args...));// 使用 std::packaged_task 来封装任务// 再使用 shared_ptr 来指向该任务,以便后续传参auto task = std::make_shared<std::packaged_task<returnType()>>(std::bind(std::forward<Func>(f), std::forward<Args>(args)...));std::future<returnType> fut = task->get_future();    // 添加任务到队列std::unique_lock<std::mutex> lck(_mtx);_queue.emplace([task](){ (*task)(); });lck.unlock();// 通知一个等待的线程_cond.notify_all();return fut;
}

是的很奇怪,不管是看起来还是用起来都是非常的奇怪,让只是熟悉 C++11 之前的版本的人来说,仿佛让自己感觉是原始人。
 首先是函数头部分:

template<class Func, class... Args>
auto addTask(Func &&f, Args&&... args)-> std::future<decltype(f(args...))>

Func 代表调用可调用的对象(函数,lambda表达式,函数对象等),Args 代表可变参数。auto 是代表推导函数返回值,那 -> std::future<decltype(f(args...))> 这是什么玩意儿呀?这代表尾返回类型,也是推导函数的返回类型。

问题一:好的,现在我知道 ->( returnType) 也是代表一个返回类型,那么为什么有了 auto 还需要未返回类型呢?不会和 auto 冲突吗?
答:我已踩坑。auto 满足大多数简单的场景,但是对于比较复杂的场景,比如这里的返回类型依赖于函数的模板参数,他无法推导出正确的类型。当两者都共存时,会首先采用尾返回类型。

问题二:std::future<decltype(f(args…))> 这是一个什么类型呢?
答:decltype 是一个关键字,可以根据表达式推导类型,所以这里 decltype(f(args…)) 的含义是:根据传入的函数以及相应的参数推导出函数返回值类型。future 代表是一个异步返回的结果。

好的我们现在来看另外一个重要的部分:

 auto task = std::make_shared<std::packaged_task<returnType()>>(std::bind(std::forward<Func>(f), std::forward<Args>(args)...));std::future<returnType> fut = task->get_future();

我们首先使用 bind 函数将该函数的参数绑定,因为我们队列中的任务对象是一个无参的。之后我们将使用 bind 封装了的函数再使用 packaged_task 封装,因为我们需要返回一个异步的结果。最后我们在使用 shared_ptr 来管理 packaged_task ,因为他是不能够被拷贝的,所以我们一共封装了三层。最后获取一个异步结果作为返回值。

现在是最后一个重要的地方了:

_queue.emplace([task](){ (*task)(); });

我们封装了一个 lambda 函数,值捕获了 task,函数的内容是执行 task 函数,因为他是一个指针,所以我们需要先解引用再执行:
在这里插入图片描述
为什么需要这样呢?因为我任务队列存储的类型是 std::function<void()>,而我们的任务对象是 std::packaged_task<returnType()> 所以还需要封装一下。


五、总结

 总结下来,难的不是逻辑,而是需要层层封装,以及需要明白为什么要这样做,还需要了解某些新特性。

相关文章:

C++ 新特性实现 ThreadPool

序言 在之前我们实现过线程池&#xff0c;但是非常基础。答题思路就是实现一个安全的队列&#xff0c;再通过 ThreadPool 来管理队列和线程&#xff0c;对外提供一个接口放入需要执行的函数&#xff0c;但是这个函数是无参无返回值的。  参数的问题我们可以使用 bind 来封装&a…...

【数据结构】_以SLTPushBack(尾插)为例理解单链表的二级指针传参

目录 1. 第一版代码 2. 第二版代码 3. 第三版代码 前文已介绍无头单向不循环链表的实现&#xff0c;详见下文&#xff1a; 【数据结构】_不带头非循环单向链表-CSDN博客 但对于部分方法如尾插、头插、任意位置前插入、任意位置前删除的相关实现&#xff0c;其形参均采用了…...

本地Harbor仓库搭建流程

Harbor仓库搭建流程 本文主要介绍如何搭建harbor仓库&#xff0c;推送本地镜像供其他机器拉取构建服务 harbor文档&#xff1a;Harbor 文档 | 配置 Harbor YML 文件 - Harbor 中文 github下载离线安装包 Releases goharbor/harbor 这是harbor的GitHub下载地址&#xff0c…...

Android vendor.img中文件执行权问题

问题 Android 9、11往vendor.img增加文件&#xff0c;烧写到设备后发现增加的可执行文件没有执行权限。经过漫长查找&#xff0c;终于找到了问题的根源&#xff0c;谨以此篇献给哪些脚踏实地的人们。 根本原因 system/core/libcutils/fs_config.cpp文件&#xff0c;fs_confi…...

环境搭建--vscode

vscode官网下载合适版本 安装vscode插件 安装 MinGW 配置环境变量 把安装目录D&#xff1a;\mingw64 配置在用户的环境变量path里即可 选择用户环境变量path 点确定保存后开启cmd输入g&#xff0c;如提示no input files 则说明Mingw64 安装成功&#xff0c;如果提示g 不是内…...

30289_SC65XX功能机MMI开发笔记(ums9117)

建立窗口步骤&#xff1a; 引入图片资源 放入图片 然后跑make pprj new job8 可能会有bug,宏定义 还会有开关灯报错&#xff0c;看命令行注释掉 接着把ture改成false 然后命令行new一遍&#xff0c;编译一遍没报错后 把编译器的win文件删掉&#xff0c; 再跑一遍虚拟机命令行…...

IDEA工具下载、配置和Tomcat配置

1. IDEA工具下载、配置 1.1. IDEA工具下载 1.1.1. 下载方式一 官方地址下载 1.1.2. 下载方式二 官方地址下载&#xff1a;https://www.jetbrains.com/idea/ 1.1.3. 注册账户 官网地址&#xff1a;https://account.jetbrains.com/login 1.1.4. JetBrains官方账号注册…...

【10.2】队列-设计循环队列

一、题目 设计你的循环队列实现。 循环队列是一种线性数据结构&#xff0c;其操作表现基于 FIFO&#xff08;先进先出&#xff09;原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。 循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普…...

多人-多agent协同可能会挑战维纳的反馈

在多人-多Agent协同系统中&#xff0c;维纳的经典反馈机制将面临新的挑战&#xff0c;而协同过程中的“算计”&#xff08;策略性决策与协调&#xff09;成为实现高效协作的核心。 1、非线性与动态性 维纳的反馈理论&#xff08;尤其是在控制理论中&#xff09;通常假设系统的动…...

JS 时间格式大全(含大量示例)

在 JS 中&#xff0c;处理时间和日期是常见的需求。无论是展示当前时间、格式化日期字符串&#xff0c;还是进行时间计算&#xff0c;JavaScript 都提供了丰富的 API 来满足这些需求。本文将详细介绍如何使用 JavaScript 生成各种时间格式&#xff0c;从基础到高级&#xff0c;…...

HarmonyOS简介:应用开发的机遇、挑战和趋势

问题 更多的智能设备并没有带来更好的全场景体验 连接步骤复杂数据难以互通生态无法共享能力难以协同 主要挑战 针对不同设备上的不同操作系统&#xff0c;重复开发&#xff0c;维护多套版本 多种语言栈&#xff0c;对人员技能要求高 多种开发框架&#xff0c;不同的编程…...

区间选点(贪心)

给定 NN 个闭区间 [ai,bi][ai,bi]&#xff0c;请你在数轴上选择尽量少的点&#xff0c;使得每个区间内至少包含一个选出的点。 输出选择的点的最小数量。 位于区间端点上的点也算作区间内。 输入格式 第一行包含整数 NN&#xff0c;表示区间数。 接下来 NN 行&#xff0c;…...

深度学习|表示学习|卷积神经网络|输出维度公式|15

如是我闻&#xff1a; 在卷积和池化操作中&#xff0c;计算输出维度的公式是关键&#xff0c;它们分别可以帮助我们计算卷积操作和池化操作后的输出大小。下面分别总结公式&#xff0c;并结合解释它们的意义&#xff1a; 1. 卷积操作的输出维度公式 当我们对输入图像进行卷积时…...

Edge-TTS在广电系统中的语音合成技术的创新应用

Edge-TTS在广电系统中的语音合成技术的创新应用 作者&#xff1a;本人是一名县级融媒体中心的工程师&#xff0c;多年来一直坚持学习、提升自己。喜欢Python编程、人工智能、网络安全等多领域的技术。 摘要 随着人工智能技术的快速发展&#xff0c;文字转语音&#xff08;Te…...

2025课题推荐——USBL与DVL数据融合的实时定位系统

准确的定位技术是现代海洋探测、海洋工程和水下机器人操作的基础。超短基线&#xff08;USBL&#xff09;和多普勒速度计&#xff08;DVL&#xff09;是常用的水下定位技术&#xff0c;但单一技术难以应对复杂环境。因此&#xff0c;USBL与DVL的数据融合以构建实时定位系统&…...

RK3588平台开发系列讲解(ARM篇)ARM64底层中断处理

文章目录 一、异常级别二、异常分类2.1、同步异常2.2、异步异常三、中断向量表沉淀、分享、成长,让自己和他人都能有所收获!😄 一、异常级别 ARM64处理器确实定义了4个异常级别(Exception Levels, EL),分别是EL0到EL3。这些级别用于管理处理器的特权级别和权限,级别越高…...

MyBatis最佳实践:提升数据库交互效率的秘密武器

第一章&#xff1a;框架的概述&#xff1a; MyBatis 框架的概述&#xff1a; MyBatis 是一个优秀的基于 Java 的持久框架&#xff0c;内部对 JDBC 做了封装&#xff0c;使开发者只需要关注 SQL 语句&#xff0c;而不关注 JDBC 的代码&#xff0c;使开发变得更加的简单MyBatis 通…...

Three.js实战项目02:vue3+three.js实现汽车展厅项目

文章目录 实战项目02项目预览项目创建初始化项目模型加载与展厅灯光加载汽车模型设置灯光材质设置完整项目下载实战项目02 项目预览 完整项目效果: 项目创建 创建项目: pnpm create vue安装包: pnpm add three@0.153.0 pnpm add gsap初始化项目 修改App.js代码&#x…...

CPP-存储区域

CPP支持手动开辟和释放内存&#xff0c;所以对于内存的理解非常重要&#xff01; 在C中&#xff0c;内存存储通常可以大致分为几个区域&#xff0c;这些区域根据存储的数据类型、生命周期和作用域来划分。这些区域主要包括&#xff1a; 代码区&#xff08;Code Segment/Text S…...

1月27(信息差)

&#x1f30d;喜大普奔&#xff0c;适用于 VS Code 的 GitHub Copilot 全新免费版本正式推出&#xff0c;GitHub 全球开发者突破1.5亿 &#x1f384;Kimi深夜炸场&#xff1a;满血版多模态o1级推理模型&#xff01;OpenAI外全球首次&#xff01;Jim Fan&#xff1a;同天两款国…...

开发环境搭建-3:配置 nodejs 开发环境 (fnm+ node + pnpm)

在 WSL 环境中配置&#xff1a;WSL2 (2.3.26.0) Oracle Linux 8.7 官方镜像 node 官网&#xff1a;https://nodejs.org/zh-cn/download 点击【下载】&#xff0c;选择想要的 node 版本、操作系统、node 版本管理器、npm包管理器 根据下面代码提示依次执行对应代码即可 基本概…...

深入理解Pytest中的Setup和Teardown

关注开源优测不迷路 大数据测试过程、策略及挑战 测试框架原理&#xff0c;构建成功的基石 在自动化测试工作之前&#xff0c;你应该知道的10条建议 在自动化测试中&#xff0c;重要的不是工具 对于简单程序而言&#xff0c;使用 Pytest 运行测试直截了当。然而&#xff0c;当你…...

一个局域网通过NAT访问另一个地址重叠的局域网(IP方式访问)

正文共&#xff1a;1335 字 7 图&#xff0c;预估阅读时间&#xff1a;4 分钟 现在&#xff0c;我们已经可以通过调整两台设备的组合配置&#xff08;地址重叠时&#xff0c;用户如何通过NAT访问对端IP网络&#xff1f;&#xff09;或仅调整一台设备的配置&#xff08;仅操作一…...

MongoDB中常用的几种高可用技术方案及优缺点

MongoDB 的高可用性方案主要依赖于其内置的 副本集 (Replica Set) 和 Sharding 机制。下面是一些常见的高可用性技术方案&#xff1a; 1. 副本集 (Replica Set) 副本集是 MongoDB 提供的主要高可用性解决方案&#xff0c;确保数据在多个节点之间的冗余存储和自动故障恢复。副…...

DeepSeek学术题目选择效果怎么样?

论文选题 一篇出色的论文背后&#xff0c;必定有一个“智慧的选题”在撑腰。选题足够好文章就能顺利登上高水平期刊&#xff1b;选题不行再精彩的写作也只能“当花瓶”。然而许多宝子们常常忽视这个环节&#xff0c;把大量时间花在写作上&#xff0c;选题时却像抓阄一样随便挑一…...

Lesson 119 A true story

Lesson 119 A true story 词汇 story n. 故事&#xff0c;传记&#xff0c;小说&#xff0c;楼层storey 搭配&#xff1a;tell a story 讲故事&#xff0c;说谎    true story 真实的故事    the second floor 二楼 例句&#xff1a;我猜他正在说谎。    I guess he…...

正反转电路梯形图

1、正转联锁控制。按下正转按钮SB1→梯形图程序中的正转触点X000闭合→线圈Y000得电→Y000自锁触点闭合&#xff0c;Y000联锁触点断开&#xff0c;Y0端子与COM端子间的内部硬触点闭合→Y000自锁触点闭合&#xff0c;使线圈Y000在X000触点断开后仍可得电。 Y000联锁触点断开&…...

Java并发学习:进程与线程的区别

进程的基本原理 一个进程是一个程序的一次启动和执行&#xff0c;是操作系统程序装入内存&#xff0c;给程序分配必要的系统资源&#xff0c;并且开始运行程序的指令。 同一个程序可以多次启动&#xff0c;对应多个进程&#xff0c;例如同一个浏览器打开多次。 一个进程由程…...

解锁罗技键盘新技能:轻松锁定功能键(罗技K580)

在使用罗技键盘的过程中&#xff0c;你是否曾因 F11、F12 功能键的默认设置与实际需求不符而感到困扰&#xff1f; 别担心&#xff0c;今天就为大家分享一个简单实用的小技巧 —— 锁定罗技键盘的 F11、F12 功能键&#xff0c;让你的操作更加得心应手&#xff01; 通常情况下…...

分布式微服务系统架构第88集:kafka集群

使用集 群最大的好处是可以跨服务器进行负载均衡&#xff0c;再则就是可以使用复制功能来避免因单点故 障造成的数据丢失。在维护 Kafka 或底层系统时&#xff0c;使用集群可以确保为客户端提供高可用 性。 需要多少个broker 一个 Kafka 集群需要多少个 broker 取决于以下几个因…...