C++写一个线程池

C++写一个线程池

文章目录

  • C++写一个线程池
    • 设计思路
    • 测试数据的实现
    • 任务类的实现
    • 线程池类的实现
      • 线程池构造函数
      • 线程池入口函数
      • 队列中取任务
      • 添加任务函数
      • 线程池终止函数
    • 源码

之前用C语言写了一个线程池,详情请见:
C语言写一个线程池

这次换成C++了!由于C++支持泛型编程,所以代码的灵活性提高了不知道多少倍!!!!!

设计思路

线程池的原理就不解释了,这次主要来讲一下我使用C++进行面向过程、面向对象、泛型设计时的思想。

线程池的工作原理是存在一个具有多个线程的空间,我们对这些线程进行一个统一的管理(利用C++提供的线程类)。然后具有一个存放任务的队列,这些线程依次从中取出任务然后执行。

从上面的过程中发现可以将线程池作为一个对象来进行设计。这个对象中的成员至少有:

  • 存放n个线程对象的空间,可以使用 vector<std::thread> 进行管理。
  • 标记每一个线程的工作状态的容器,这里可以使用 unordered_map<<std::thread::id, bool>来进行管理。
  • 存放任务的队列
  • 等等…(在设计的过程中会发现)

我希望设计一个很万能的线程池,即可以接受任何任务,我只需要将对应的函数和参数传入进队列中,就可以让线程自动执行。

因此,设计一个任务类是必不可少的。因为,我们从函数外部传进去的函数和参数不一定相同,而且不同功能的任务之间没有一个合适的管理方式,因此我们需要设计一个任务类来兼容不同参数,并且将参数和工作函数绑定到一块的情况。

测试数据的实现

我个人比较喜欢在设计代码之前假设他已经设计好,然后先写出他的测试方法和数据,之后一点点来实现,这次我选择的测试方法是求1~50000000中的素数个数,不使用素数筛:

先实现判断素数的功能函数:

// 判断素数n是不是素数
bool is_prime(int n) {for (int i = 2; i * i < n; ++i) {if (n % i == 0) return false;}return true;
}// 求出 start 到 end 返回的素数个数
int prime_count(int start, int end) {int ans = 0;for (int i = start; i <= end; ++i) {ans += is_prime(i);}return ans;
}// 需要传入到线程池内的函数,求出 l 到 r 的素数个数然后保存在 ret 中
void worker(int l, int r, int &ret) {cout << this_thread::get_id() << ": begin" << endl;ret = prime_count(l, r);cout << this_thread::get_id() << ": end" << endl;return ;
}

下面是主函数:

int main() {#define MAX_N 5000000  // 这里假设需要处理10次,因此每次处理五百万的数据thread_pool tp(5);     // 假设传入的参数是线程池中的线程个数int ret[10];           // 存放10次结果for (int i = 0. j = 1; i < 10; ++i, j += MAX_N) {tp.add_task(worker, j, j + MAX_N - 1, ref(ret[i]));  // ref是用来传入引用的}tp.stop();              // 停止线程池的运转int ans = 0;            // 计算出结果for (int i = 0; i < 10; ++i) {ans += ret[i];}cout << "ans = " << ans << endl;return 0;
}

任务类的实现

明确一下目的:
实现一个类,第一个参数是一个函数地址,后面为变参列表,该类会将函数与参数打包成一个新的函数,作为任务队列中的一个元素,当空闲线程将其取出之后,可以执行这个新的函数。

这个需要用到模板:

template <typename FUNC_T, typename ...ARGS>
class Task {Task(FUNC_T func, ARGS... args) {...}
private:...
};

绑定之后我们需要一个变量来存放这个函数,因此需要添加一个函数指针对象 function<void()>
使用 bind 函数进行绑定。
在给bind函数传入参数列表时,需要维持左右值原型,因此需要工具类 std::forward<ARGS>(args)... 来解析参数类型。

template <typename FUNC_T, typename ...ARGS>
class Task {Task(FUNC_T func, ARGS... args) {this->_func = bind(functionn, std::forward<ARGS>(args)...);}
private:std::function<void()> _func;
};

最后需要一个方法来运行这个函数:
别忘了析构函数。

template <typename FUNC_T, typename ...ARGS>
class Task {Task(FUNC_T func, ARGS... args) {this->_func = bind(functionn, std::forward<ARGS>(args)...);}~Task() {delete _func;}void run() {_func();return ;}
private:std::function<void()> _func;
};

线程池类的实现

根据一开始的测试数据,发现线程池的操作对外就支持两个操作:

  • 压入任务
  • 停止

根据一开始所分析的:

  • 存放n个线程对象的空间,可以使用 vector<std::thread> 进行管理。
  • 标记每一个线程的工作状态的容器,这里可以使用 unordered_map<std::thread::id, bool>来进行管理。
  • 存放任务的队列
  • 等等…(在设计的过程中会发现)

我们可以先将成员和已知的方法写上:

template <typename FUNC_T, typename ...ARGS>
class thread_pool {
public:thread_pool() {}template <typename FUNC_T, typename ...ARGS>void add_task(FUNC_T func, ARGS... args) {...}void stop() {...}private:std::vector<std::thread *> _threads;unordered_map<std::thread::id, bool> _running;std::queue<Task *> _tasks;
};

线程池构造函数

先来尝试完善一下构造函数,我们使用参数来控制线程池中的线程个数,默认线程数量我们可以设置成为1

创建出的线程空间放在堆区,因此使用 new 关键字来创建:

thread_pool(int n = 1) {for(int i = 0; i < n; ++i) {_threads[i] = new thread(&thread_pool::worker, this); // 别忘了内部方法的第一个隐藏参数this传入进去}
}

线程池入口函数

这个时候我们发现,由于thread的构造函数需要传入一个需要运行的函数,因此发现又多了一个函数就是工作函数 worker

简单来说,这个函数的功能规定了所有线程的行为——从队列中取出任务并执行。

在工作函数内部,我们需要将该线程 id 记录下来(表示他是否存活),然后不断判断这个线程是否存活,如果存活就继续等待队列中的任务

这个工作函数的作用就是不断检查队列中是否有可以取出的任务,然后执行。

void worker() {auto id = this_thread::get_id();_running[id] = true; // 表示这个线程被记录下来,在运行状态while (_running[id]) {Task *t = get_task();   // 从队列中取任务,这里又诞生出一个新函数t->run();               // 运行任务delete t;}
}

队列中取任务

可以发现又有了新的函数需求就是从队列中取出一个任务。

这个函数并不对外表现,所以应该设置为私有成员方法

主要逻辑就是检查队列头部是否有任务对象,如果有的话就返回这个任务的地址。

由于队列是临界资源,所以需要上锁,此时不免又多了两个成员变量

std::mutex m_mutex;
std::condition_variable m_cond;

Task *get_task() {unique_task<mutex> (m_mutex); // 上锁while (_tasks.empty()) {      // 如果队列为空则释放锁并等待队列被放入任务的条件m_cond.wait(lock)}Task *t = _tasks.front();_tasks.pop();return t;
}

这样一来,我们就实现了线程的初始化以及任务的取出

接下来,还剩下任务的压入,这个操作由 add_task() 实现,因此我们先来实现这个函数

添加任务函数

由于他也是访问临界资源,因此,也需要上锁,同时在添加成功之后释放一个信号。

同样的,这个函数需要在外部调用,因此设置成为共有成员方法:

template <typename FUNC_T, typename ...ARGS>
void add_task(FUNC_T func, ARGS... args) {_tasks.push(new Task(func, std:forward<ARGS>(args)...));lock.unlock();m_cond.notify_one();return ;
}

线程池终止函数

线程不再运行之后可以选择终止他们来节省计算机资源,因此这个函数是必不可少的,主要的操作方式如下,我们想队列中压入等于同于线程数量的特殊任务这个特殊任务会把线程标记为非活动的,然后后等到他们全部执行完任务后,再依次释放掉他们的资源。

void stop() {for (int i = 0; i < _thread.size(); ++i) {this->add_task(&thread_pool::stop_running, this);   // 毒药方法}for(int i = 0; i < _thread_size(); ++i) {_threads[i]->join();}for(int i = 0; i < _thread.size(); ++i) {delete _threads[i];_threads[i] = nullptr;}return ;
}

其中涉及到了 stop_running() 这个毒药方法,这个方法只在函数内部调用,因此我们把他设计成为私有成员方法。

这个函数的逻辑就是将当前线程标记为非活动的状态。

void stop_running() {auto id = this_thread::get_id();_running[id] = false;return ;
}

到此为止,线程池的大部分功能就设计的差不多了,之后我又进行了一下细微的调整,相信读者应该自己也能读懂,这里就不过多解释了。

源码

#include <condition_variable>
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <stack>
#include <set>
#include <map>
#include <cstdio>
#include <cstdlib>
#include <ctype.h>
#include <cmath>
#include <string>
#include <sstream>
#include <functional>
#include <thread>
#include <mutex>
#include <condition_variable>#define TEST_BEGINS(x) namespace x {
#define TEST_ENDS(x) } // end of namespace x
using namespace std;TEST_BEGINS(thread_pool)bool is_prime(int n) {for (int i = 2; i * i <= n; ++i) {if (n % i == 0) return false;}return true;
}int prime_count(int start, int end) {int ans = 0;for (int i = start; i <= end; ++i) {ans += is_prime(i);}return ans;
}// 多线程入口函数
void worker(int l, int r, int &ret) {cout << this_thread::get_id() << " : begin" << endl;ret = prime_count(l, r);cout << this_thread::get_id() << " : done" << endl;return ;
}class Task {
public:template <typename FUNC_T, typename ...ARGS>Task(FUNC_T function, ARGS ...args) {this->func = bind(function, std::forward<ARGS>(args)...);}void run() {func();return ;}
private:function<void()> func;
};class thread_pool {
public:thread_pool(int n = 1) : _thread_size(n), _threads(n), starting(false) {this->start();}~thread_pool() {this->stop();while (!_tasks.empty()) {delete _tasks.front();_tasks.pop();}return ;}/** 入口函数:不断从队列中取任务,然后运行,最后释放资源。*/void worker() {auto id = this_thread::get_id();_running[id] = true;while (_running[id]) {Task *t = get_task();t->run();delete t;}return ;}void start() {if (starting == true) return ;for (int i = 0; i < _thread_size; ++i) {_threads[i] = new thread(&thread_pool::worker, this);}starting = true;          // 标记线程池运行return ;}void stop() {if (starting == false) return ;for (int i = 0; i < _threads.size(); ++i) {this->add_task(&thread_pool::stop_running, this);}for (int i = 0; i < _threads.size(); ++i) {_threads[i]->join();}for (int i = 0; i < _threads.size(); ++i) {delete _threads[i];_threads[i] = nullptr;}starting = false;return ;}template <typename FUNC_T, typename ...ARGS>void add_task(FUNC_T func, ARGS... args) {unique_lock<mutex> lock(m_mutex);_tasks.push(new Task(func, std::forward<ARGS>(args)...));lock.unlock();m_cond.notify_one();return ;}private:Task *get_task() {unique_lock<mutex> lock(m_mutex);while (_tasks.empty()) {          // 避免虚假唤醒m_cond.wait(lock);}Task *t = _tasks.front();_tasks.pop();return t;}void stop_running() {auto id = this_thread::get_id();_running[id] = false;                  // 毒药方法return ;}bool starting;int _thread_size;  													// 记录线程个数std::mutex m_mutex;													// 互斥锁std::condition_variable m_cond; 									// 条件变量vector<thread *> _threads;  										// 线程区unordered_map<std::thread::id, bool> _running;   					// 线程活动标记  queue<Task *> _tasks; 												// 任务队列
};int main() {#define MAX_N 5000000thread_pool tp(10);int ret[10];for (int i = 0, j = 1; i < 10; ++i, j += batch) {tp.add_task(worker, j, j + MAX_N - 1, ref(ret[i]));}tp.stop();int ans = 0;for (auto x : ret) ans += x;cout << ans << endl;return 0;
}TEST_ENDS(thread_pool)int main() {thread_pool::main();return 0;
}

运行结果:
1

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

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

相关文章

object-C 解答算法:合并两个有序数组(leetCode-88)

合并两个有序数组(leetCode-88) 题目如下图:(也可以到leetCode上看完整题目,题号88) 首先搞懂,什么叫“非递减顺序” 非递减顺序,是指一个序列中的元素从前往后&#xff08;或从左到右&#xff09;保持不减少或相等。 这意味着序列中的元素可以保持相同的值&#xff0c;但不会…

网络云服务1

第一章 虚拟私有云 前言 在整个ICT基础设施的发展过程中&#xff0c;网络资源一直是必不可少的存在。有了网络资源&#xff0c;设备与设备间&#xff0c;系统与系统间才有了交流&#xff0c;才能更好地去支撑企业业务的快速发展。本章将带领大家了解华为云上的网络服务。 网…

四个节点即可实现的ComfyUI批量抠图工作流

原文链接&#xff1a;ComfyUI面部修复完全指南 (chinaz.com) 下图就是批量抠图的工作流 虽然工作流很简单&#xff0c;但是我们前提还是需要安装好我们的节点 首先安装我们的抠图节点 安装 BiRefNet 所需依赖&#xff1a;timm&#xff0c;如已安装无需运行 requirements.txt…

如何写好建模论文

如何写好建模论文 一、 写好数模答卷的重要性 1.评定参赛队的成绩好坏、高低&#xff0c;获奖级别&#xff0c; 数模答卷&#xff0c; 是唯一依据。 2.答卷是竞赛活动的成绩结晶的书面形式。 3.写好答卷的训练&#xff0c;是科技写作的一种基本训练。 二、 答卷的基本内容&…

【HTML入门】第十五课 - form表单(下)表单控件们(二)

上一小节我们说了文本输入框&#xff0c;密码输入框&#xff0c;数值型输入框&#xff0c;还有大的文本域。这一小节&#xff0c;我们继续说form表单中的一些常用的控件们。 目录 1 单选按钮 2 复选框 3 下拉列表选择 1 单选按钮 单选按钮&#xff0c;就是说一组按钮中&am…

R-CNN 中的区域建议网络

区域建议网络&#xff08;Region Proposal Network&#xff0c;RPN&#xff09;是R-CNN&#xff08;Regions with Convolutional Neural Networks&#xff09;架构中的一个关键组件&#xff0c;特别是在Faster R-CNN中。RPN的主要任务是生成可能包含物体的区域提议&#xff0c;…

buuctf-reverse write-ups (2)

文章目录 buu097-[SUCTF2019]hardcpp状态变量常量值与基本块的对应关系状态变量更新还原控制流程序修复进一步调试修复效果全局变量混淆去除 buu097-[SUCTF2019]hardcpp 这是一个简单的C程序&#xff0c;但带有大量的控制流平坦化混淆。下面我将从头开始编写用于解决此类混淆问…

FairGuard游戏加固入选《嘶吼2024网络安全产业图谱》

2024年7月16日&#xff0c;国内网络安全专业媒体——嘶吼安全产业研究院正式发布《嘶吼2024网络安全产业图谱》(以下简称“产业图谱”)。 本次发布的产业图谱&#xff0c;共涉及七大类别&#xff0c;127个细分领域。全面展现了网络安全产业的构成和重要组成部分&#xff0c;探…

react 快速入门思维导图

在掌握了react中一下的几个步骤和语法&#xff0c;基本上就可以熟练的使用react了。 1、组件的使用。react创建组件主要是类组件和函数式组件&#xff0c;类组件有生命周期&#xff0c;而函数式组件没有。 2、jsx语法。react主要使用jsx语法&#xff0c;需要使用babel和webpa…

二染色,CF 1594D - The Number of Imposters

目录 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 二、解题报告 1、思路分析 2、复杂度 3、代码详解 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 1594D - The Number of Imposters 二、解题报告 1、思路分析 并查集&…

uniapp封装请求拦截器,封装请求拦截和响应拦截的方法

首先我们先看一下uni官方给开发者提供的uni.request用来网络请求的api 1 2 3 4 5 6 7 8 9 uni.request({ url: , method: GET, data: {}, header: {}, success: res > {}, fail: () > {}, complete: () > {} }); 可以看到我们每次请求数据的时候都需…

通过MATLAB控制TI毫米波雷达的工作状态之TLV数据解析及绘制

前言 前一章博主介绍了如何基于设计视图中的这些组件结合MATLAB代码来实现TI毫米波雷达数据的实时采集。这一章将在此基础上实现TI毫米波雷达的TLV数据解析。过程中部分算法会涉及到一些简单的毫米波雷达相关算法,需要各位有一定的毫米波雷达基础。 TLV数据之协议解析 紧着…

el-cascader数据回显失败

el-cascader选中数据第一次回显正常&#xff0c;当选中数据改变再次回显时失败&#xff0c;呈现的还是上次的选中数据 如图 常用的方法this. n e x t T i c k ( ( ) > ) 跟 t h i s . nextTick(() > {})跟this. nextTick(()>)跟this.forceUpdate();强制刷新数据都无…

Hadoop-37 HBase集群 JavaAPI 操作3台云服务器 POM 实现增删改查调用操作 列族信息 扫描全表

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; HadoopHDFSMapReduceHiveFlumeSqoopZookeeperHBase 正在 章节内容 上一节我们完成了&#xff1a; HBase …

关于dom4j主节点的xmlns无法写入的问题

由于最近需要做一个xml的文件&#xff0c;使用dom4j的时候发现了一个bug&#xff0c;就是我的xmlns根本无法写入到xml的头部标签中。 Element element document.addElement("test"); element.addAttribute("xmlns", "urn:Declaration:datamodel:sta…

【中项第三版】系统集成项目管理工程师 | 第 5 章 软件工程① | 5.1 - 5.3

前言 第5章对应的内容选择题和案例分析都会进行考查&#xff0c;这一章节属于技术的内容&#xff0c;学习要以教材为准。 目录 5.1 软件工程定义 5.2 软件需求 5.2.1 需求的层次 5.2.2 质量功能部署 5.2.3 需求获取 5.2.4 需求分析 5.2.5 需求规格说明书 5.2.6 需求变…

人工智能算法工程师(中级)课程12-PyTorch神经网络之LSTM和GRU网络与代码详解1

大家好,我是微学AI,今天给大家介绍一下人工智能算法工程师(中级)课程12-PyTorch神经网络之LSTM和GRU网络与代码详解。在深度学习领域,循环神经网络(RNN)因其处理序列数据的能力而备受关注。然而,传统的RNN存在梯度消失和梯度爆炸的问题,这使得它在长序列任务中的表现不尽…

高速网络技术变革,RoCE取代IB之路

RoCE取代IB&#xff1a;为何之前是IB&#xff0c;现在是RoCE&#xff1f; 以太网在AI算力中的Why、How和What”。 超以太网联盟&#xff08;UEC&#xff09;由Linux基金会和联合开发基金会共同发起&#xff0c;旨在超越传统以太网功能。通过RDMA和RoCE等技术&#xff0c;UEC为…

SMTP服务器地址与端口号有哪些关系与区别?

SMTP服务器地址如何正确配置&#xff1f;怎么验证服务器的地址&#xff1f; 了解SMTP服务器地址与端口号的关系与区别对于确保邮件系统的正常运作至关重要。AokSend将详细探讨这两者之间的关系和区别&#xff0c;并解释它们在邮件传输过程中的重要性。 SMTP服务器地址&#x…

PHP萌宠之家微信小程序系统源码

&#x1f43e;萌宠之家微信小程序&#x1f43e; —— 铲屎官们的温馨小窝✨ &#x1f3e0;【一键开启萌宠乐园】&#x1f3e0; 亲们&#xff0c;是不是每次刷手机都忍不住想看看那些软萌可爱的毛孩子&#xff1f;现在&#xff0c;有了“萌宠之家”微信小程序&#xff0c;你的…