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 + 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()> 所以还需要封装一下。


五、总结

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

网络攻防实战指北专栏讲解大纲与网络安全法

专栏 本专栏为网络攻防实战指北&#xff0c;大纲如下所示 进度&#xff1a;目前已更完准备篇、HTML基础 计划&#xff1a;所谓基础不牢&#xff0c;地动山摇。所以下一步将持续更新基础篇内容 讲解信息安全时&#xff0c;结合《中华人民共和国网络安全法》&#xff08;以下简…

计算机网络——流量控制

流量控制的基本方法是确保发送方不会以超过接收方处理能力的速度发送数据包。 通常的做法是接收方会向发送方提供某种反馈&#xff0c;如&#xff1a; &#xff08;1&#xff09;停止&等待 在任何时候只有一个数据包在传输&#xff0c;发送方发送一个数据包&#xff0c;…

知识库管理系统助力企业实现知识共享与创新价值的转型之道

内容概要 知识库管理系统&#xff08;KMS&#xff09;作为现代企业知识管理的重要组成部分&#xff0c;其定义涵盖了系统化捕捉、存储、共享和应用知识的过程。这类系统通过集成各种信息来源&#xff0c;不仅为员工提供了一个集中式的知识平台&#xff0c;还以其结构化的方式提…

⼆叉树的存储(上)c++

在前几天写的树&#xff0c;我们已经了解到树的存储&#xff0c;⼆叉树也是树&#xff0c;也是可以⽤vector数组或者链式前向星来存储。仅需在存储的过程中标记谁是左孩⼦&#xff0c;谁是右孩⼦即可。 ⽐如⽤ vector 数组存储时&#xff0c;可以先尾插左孩⼦&#xff0c;再尾…

2025创业思路和方向有哪些?

创业思路和方向是决定创业成功与否的关键因素。以下是一些基于找到的参考内容的创业思路和方向&#xff0c;旨在激发创业灵感&#xff1a; 一、技术创新与融合&#xff1a; 1、智能手机与云电视结合&#xff1a;开发集成智能手机功能的云电视&#xff0c;提供通讯、娱乐一体化体…

研发的护城河到底是什么?

0 你的问题&#xff0c;我知道&#xff01; 和大厂朋友聊天&#xff0c;他感叹原来努力干活&#xff0c;做靠谱研发&#xff0c;积累职场经验&#xff0c;干下来&#xff0c;职业发展一般问题不大。而如今大厂“年轻化”&#xff0c;靠谱再不能为自己续航&#xff0c;企业似乎…

FreeRTOS从入门到精通 第十五章(事件标志组)

参考教程&#xff1a;【正点原子】手把手教你学FreeRTOS实时系统_哔哩哔哩_bilibili 一、事件标志组简介 1、概述 &#xff08;1&#xff09;事件标志位是一个“位”&#xff0c;用来表示事件是否发生。 &#xff08;2&#xff09;事件标志组是一组事件标志位的集合&#x…

学习数据结构(5)单向链表的实现

&#xff08;1&#xff09;头部插入 &#xff08;2&#xff09;尾部删除 &#xff08;3&#xff09;头部删除 &#xff08;4&#xff09;查找 &#xff08;5&#xff09;在指定位置之前插入节点 &#xff08;6&#xff09;在指定位置之后插入节点 &#xff08;7&#xff09;删除…

Golang :用Redis构建高效灵活的应用程序

在当前的应用程序开发中&#xff0c;高效的数据存储和检索的必要性已经变得至关重要。Redis是一个快速的、开源的、内存中的数据结构存储&#xff0c;为各种应用场景提供了可靠的解决方案。在这个完整的指南中&#xff0c;我们将学习什么是Redis&#xff0c;通过Docker Compose…

18 大量数据的异步查询方案

在分布式的应用中分库分表大家都已经熟知了。如果我们的程序中需要做一个模糊查询&#xff0c;那就涉及到跨库搜索的情况&#xff0c;这个时候需要看中间件能不能支持跨库求交集的功能。比如mycat就不支持跨库查询&#xff0c;当然现在mycat也渐渐被摒弃了(没有处理笛卡尔交集的…

Redis代金卷(优惠卷)秒杀案例-单应用版

优惠卷表:优惠卷基本信息,优惠金额,使用规则 包含普通优惠卷和特价优惠卷(秒杀卷) 优惠卷的库存表:优惠卷的库存,开始抢购时间,结束抢购时间.只有特价优惠卷(秒杀卷)才需要填写这些信息 优惠卷订单表 卷的表里已经有一条普通优惠卷记录 下面首先新增一条秒杀优惠卷记录 { &quo…

编程题-三数之和(中等)

题目&#xff1a; 给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 nums[i] nums[j] nums[k] 0 。请你返回所有和为 0 且不重复的三元组。注意&#xff1a;答案中不可以包含重复的三…

过年回家的意义,

年前&#xff0c;我特想让家里人到深圳过年&#xff0c;想让他们看看深圳&#xff0c;看看外面好玩的样子&#xff0c;跟我妈提了两次&#xff0c;她两次的借口都是家里养着的那几头猪&#xff0c;还有好多鸡鸭要喂&#xff0c;说家里一定得要有人守&#xff0c;人走远是不行的…

一文读懂 Faiss:开启高维向量高效检索的大门

一、引言 在大数据与人工智能蓬勃发展的当下&#xff0c;高维向量数据如潮水般涌现。无论是图像、音频、文本&#xff0c;还是生物信息领域&#xff0c;都离不开高维向量来精准刻画数据特征。然而&#xff0c;在海量的高维向量数据中进行快速、准确的相似性搜索&#xff0c;却…

扩展无限可能:Obsidian Web Viewer插件解析

随着 Obsidian 1.8.3 正式版的发布&#xff0c;备受期待的官方核心插件——Web Viewer 也终于上线。本文将从插件启用、设置以及应用场景三个方面详细介绍如何使用这一新功能&#xff0c;和大家一起更好地利用 Obsidian 进行内容管理和知识整理。 插件启用 Web Viewer作为官方…

22.Word:小张-经费联审核结算单❗【16】

目录 NO1.2 NO3.4​ NO5.6.7 NO8邮件合并 MS搜狗输入法 NO1.2 用ms打开文件&#xff0c;而不是wps❗不然后面都没分布局→页面设置→页面大小→页面方向→上下左右&#xff1a;页边距→页码范围&#xff1a;多页&#xff1a;拼页光标处于→布局→分隔符&#xff1a;分节符…

仿真设计|基于51单片机的贪吃蛇游戏

目录 具体实现功能 设计介绍 51单片机简介 资料内容 仿真实现&#xff08;protues8.7&#xff09; 程序&#xff08;Keil5&#xff09; 全部内容 资料获取 具体实现功能 利用单片机8*8点阵实现贪吃蛇游戏的控制。 仿真演示视频&#xff1a; 51-基于51单片机的贪吃蛇游…

【HarmonyOS之旅】基于ArkTS开发(三) -> 兼容JS的类Web开发(二)

目录 1 -> HML语法 1.1 -> 页面结构 1.2 -> 数据绑定 1.3 -> 普通事件绑定 1.4 -> 冒泡事件绑定5 1.5 -> 捕获事件绑定5 1.6 -> 列表渲染 1.7 -> 条件渲染 1.8 -> 逻辑控制块 1.9 -> 模板引用 2 -> CSS语法 2.1 -> 尺寸单位 …

CPU、GPU、NPU

文章目录 内存、带宽、时延&#xff1a;尽可能提高算力的利用率&#xff01;AI 芯片基础 内存、带宽、时延&#xff1a;尽可能提高算力的利用率&#xff01; CPU计算本质&#xff1a;数据如何传输【AI芯片】芯片基础03 横坐标&#xff1a;算力敏感度&#xff0c;每次操作能执…

11.QT控件:输入类控件

1. Line Edit(单行输入框) QLineEdit表示单行输入框&#xff0c;用来输入一段文本&#xff0c;但是不能换行。 核心属性&#xff1a; 核心信号&#xff1a; 2. Text Edit(多行输入框) QTextEdit表示多行输入框&#xff0c;也是一个富文本 & markdown编辑器。并且能在内容超…