C++ 新特性实现 ThreadPool

news2025/5/11 10:07:10

序言

 在之前我们实现过线程池,但是非常基础。答题思路就是实现一个安全的队列,再通过 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 + y
	return;
}

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_task
    std::packaged_task<int(int, int)> tsk(add);
    // 获取与之相对应的 future
    std::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()> 所以还需要封装一下。


五、总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2284373.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据结构】_以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…

环境搭建--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;通常假设系统的动…

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

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

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…

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包管理器 根据下面代码提示依次执行对应代码即可 基本概…

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

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

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

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

正反转电路梯形图

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

高可用集群故障之join

本文记录了在部署高可用的k8s集群时&#xff0c;遇到的一个故障及其解决方法。 集群环境 描述&#xff1a;三主三从&#xff0c;eth0为外网网卡&#xff0c;eth1为内网网卡&#xff0c;内网互通。 需求&#xff1a;eth0只负责访问外网&#xff0c;eth1作为集群间的通信。 主…

【Web开发】一步一步详细分析使用Bolt.new生成的简单的VUE项目

https://bolt.new/ 这是一个bolt.new生成的Vue小项目&#xff0c;让我们来一步一步了解其架构&#xff0c;学习Vue开发&#xff0c;并美化它。 框架: Vue 3: 用于构建用户界面。 TypeScript: 提供类型安全和更好的开发体验。 Vite: 用于快速构建和开发 主界面如下&#xff1a…