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;
}
运行结果:
相关文章:
C++写一个线程池
C写一个线程池 文章目录 C写一个线程池设计思路测试数据的实现任务类的实现线程池类的实现线程池构造函数线程池入口函数队列中取任务添加任务函数线程池终止函数 源码 之前用C语言写了一个线程池,详情请见: C语言写一个线程池 这次换成C了!…...
【SASS/SCSS(一)】选择器
Sass 是一门高于 CSS 的元语言,它能用来清晰地、结构化地描述文件样式。 而SCSS是SASS引入的语法,是CSS的超集,所以所有CSS有效的使用在SCSS中都生效 一、回顾CSS选择器 通用选择器 *元素选择器类选择器,.classNameID选择器&am…...
详细解析Kafaka Streams中各个DSL操作符的用法
什么是DSL? 在Kafka Streams中,DSL(Domain Specific Language)指的是一组专门用于处理Kafka中数据流的高级抽象和操作符。这些操作符以声明性的方式定义了数据流的转换、聚合、连接等处理逻辑,使得开发者可以更加专注…...
C++中链表的底层迭代器实现
大家都知道在C的学习中迭代器是必不可少的,今天我们学习的是C中的链表的底层迭代器的实现,首先我们应该先知道链表的底层迭代器和顺序表的底层迭代器在实现上有什么区别,为什么顺序表的底层迭代器更加容易实现,而链表的底层迭代器…...
3.5、matlab打开显示保存点云文件(.ply/.pcd)以及经典点云模型数据
1、点云数据简介 点云数据是三维空间中由大量二维点坐标组成的数据集合。每个点代表空间中的一个坐标点,可以包含有关该点的颜色、法向量、强度值等额外信息。点云数据可以通过激光扫描、结构光扫描、摄像机捕捉等方式获取,广泛应用于计算机视觉、机器人…...
Qt-事件与信号
事件和信号的区别在于,事件通常是由窗口系统或应用程序产生的,信号则是Qt定义或用户自定义的。Qt为界面组件定义的信号往往通常是对事件的封装,如QPushButton的clicked()信号可以看做对QEvent::MouseButtonRelease类事件的封装。 在使用界面组…...
数据结构 day3
目录 思维导图: 学习内容: 1. 顺序表 1.1 概念 1.2 有关顺序表的操作 1.2.1 创建顺序表 1.2.2 顺序表判空和判断满 1.2.3 向顺序表中添加元素 1.2.4 遍历顺序表 1.2.5 顺序表按位置进行插入元素 1.2.6 顺序表任意位置删除元素 1.2.7 按值进…...
Kubernetes面试整理-如何进行滚动更新和回滚?
在 Kubernetes 中,滚动更新和回滚是管理应用程序版本的常用操作。滚动更新允许您逐步替换现有的 Pod 实例,以便在不中断服务的情况下部署新版本。回滚则是在新版本出现问题时恢复到之前的版本。 滚动更新 通过 Deployment 进行滚动更新 1. 创建一个 Deployment: 下面是一个…...
flutter ios打包 xcode报错module ‘xxx‘ not found
flutter ios打包 xcode报错module ‘xxx’ not found 如果已经在androidstudio中成功运行了flutter build ios --release。 那么可能是你使用xcode打开的是ios/Runner.xcodeproj文件。 你关掉xcode,重新打开ios/Runner.xcworkspace/文件。然后重新archiveÿ…...
LLM 构建Data Multi-Agents 赋能数据分析平台的实践之④:数据分析之三(数据展示)
概述 在先前探讨的文章中,我们构建了一个全面的数据测试体系,该体系遵循“数据获取—数据治理—数据分析”的流程。如何高效地构建数据可视化看板,以直观展现分析结果,正逐渐成为利用新兴技术提升效能的关键领域。伴随业务拓展、数…...
Elasticsearch 批量更新
Elasticsearch 批量更新 准备条件查询数据批量更新 准备条件 以下查询操作都基于索引crm_flow_info来操作,索引已经建过了,本文主要讲Elasticsearch批量更新指定字段语句,下面开始写更新语句执行更新啦! 查询数据 查询指定shif…...
【Pytorch笔记】张量
torch.Tensor() 是 PyTorch 库中用于创建张量的一个函数。在 PyTorch 中,张量是多维数组,它们可以存储在 CPU 或 GPU 上,并且支持自动求导,这使得它们非常适合进行深度学习和科学计算。 张量可以在Python list形式下通过 torch.T…...
查找json中指定节点的值,替换为指定的值
有时我们封装好的实体转化成的json字段的值和第三方要求的不一样,比如我们字段的值是字符串,我们需要使用int类型的值,就需要将这个键的值转化成int类型。 比如将以下 convulsionNumber字段中字符串的值“一次”替换为0 {"convulsionT…...
Android 14 开机时间优化措施
Android开机优化系列文档-CSDN博客 Android 14 开机时间优化措施汇总-CSDN博客Android 14 开机时间优化措施-CSDN博客根据systrace报告优化系统时需要关注的指标和优化策略-CSDN博客Android系统上常见的性能优化工具-CSDN博客Android上如何使用perfetto分析systrace-CSDN博客A…...
【QGroundControl二次开发】二.使用QT编译QGC(Windows)
【QGroundControl二次开发】一.开发环境准备(Windows) 二. 使用QT编译QGC(Windows) 2.1 打开QT Creator,选择打开项目,打开之前下载的QGC项目源码。 编译器选择Desktop Qt 6.6.3 MSVC2019 64bit。 点击运…...
[C/C++入门][变量和运算]4、带余除法
给定被除数和除数,求整数商及余数 看到这个题,我们都知道C的除法运算符 /,默认是不带余数的。那现在要求带余数,需要能够想到% %,是C获取余数的方法:比如5/22; 5%21;%得到的是除后的余数。 #inc…...
常用优秀内网穿透工具(实测详细版)
文章目录 1、前言2、安装Nginx3、配置Nginx4、启动Nginx服务4.1、配置登录页面 5、内网穿透5.1、cpolar5.1.1、cpolar软件安装5.1.2、cpolar穿透 5.2、Ngrok5.2.1、Ngrok安装5.2.2、随机域名5.2.3、固定域名5.2.4、前后端服务端口 5.3、NatApp5.4、Frp5.4.1、下载Frp5.4.2、暴露…...
防火墙NAT地址转换和智能选举综合实验
一、实验拓扑 目录 一、实验拓扑 二、实验要求(接上一个实验要求后) 三、实验步骤 3.1办公区设备可以通过电信链路和移动链路上网(多对多的NAT,并且需要保留一个公网IP不能用来转换) 3.2分公司设备可以通过总公司的移动链路和电信链路访…...
Android获取当前屏幕显示的是哪个activity
在 Android 中,要获取当前屏幕显示的 Activity,可以使用以下几种方法: 方法一:使用 ActivityManager 获取当前运行的任务信息 这是一个常见的方法,尽管从 Android 5.0 (API 21) 开始,有些方法变得不太可靠…...
JVM:自动垃圾回收
文章目录 一、C/C的内存管理二、Java的内存管理1、方法去的回收2、堆回收(1)引用计数法和可达性分析法(2)五种对象引用(3)垃圾回收算法 一、C/C的内存管理 在C和C没有自动垃圾回收机制,一个对象…...
【填坑指南】PHP8报:Unable to load dynamic library ‘zip.so’ 错误
1.原因分析 这种情况多数发生在PHP安装时因为各种原因失败后,残余的库与最后安装的PHP版本不兼容导致的。 2.我的路径 一开始我按照以前摸索出来的安装PHP7.3的成功经验来编译方法安装PHP8.3,发现以前的套路已经失效了。反复重装PHP8.3失败后…...
鸿蒙语言基础类库:【@system.notification (通知消息)】
通知消息 说明: 从API Version 7 开始,该接口不再维护,推荐使用新接口[ohos.notification]。本模块首批接口从API version 3开始支持。后续版本的新增接口,采用上角标单独标记接口的起始版本。 导入模块 import notification fro…...
1.JavaWeb开发简介(Tomcat安装使用+Servlet简介)
文章目录 一.web开发简介1.概念:2.特点:3.常用技术:4.服务架构5.web应用开发模式6.HTTP协议1)概念:2)HTTP最基本的过程是:3)IP/域名4)HTTP协议请求方式 7.JavaWeb的相关技术8.Java Web服务器 二、安装配置Tomcat1.简介2.Tomcat目录结构 三.Servlet的入门应用1.使用步骤2.使用注…...
xxl-job 动态创建一次性定时任务
文章目录 需求一、考虑方案二、实现思路三、代码实现3.1 引入xxl-core 核心包3.2 远程调用3.2.0 yaml3.2.1 配置类3.2.2 入参3.2.3 任务返回实体3.2.4 任务调用 3.3 cron生成器3.4 handler实现3.4 测试 踩坑 需求 类似预约会议,设置提醒 添加数据记录(…...
网页制作技术:概念、现状与展望?
网页制作技术:概念、现状与展望? 李升伟 网页制作技术是指用于创建和维护网站的一系列技术和方法。 概念: 它涉及多个方面,包括使用 HTML(超文本标记语言)来构建网页的结构和内容,使用 CSS&…...
Kafka Producer之数据重复和乱序问题
文章目录 1. 数据重复2. 数据乱序 为了可靠性,Kafka有消息重试机制,但是同时也带来了2大问题 1. 数据重复 消息发送到broker后,broker记录消息数据到log中,但是由于网络问题,producer没有收到acks,于是再次…...
Java前后端分离开发的步骤以及注意事项
在现代Web应用程序开发中,前后端分离是一种常见的架构模式。这种模式将前端(用户界面)和后端(业务逻辑和数据处理)分开独立开发和部署,从而提高开发效率、代码的可维护性和团队协作能力。本文将介绍Java前后…...
C#绘制阻抗圆图初步
阻抗圆图,或者叫史密斯图,是无线电设计方面用的; 基本的阻抗圆图如下, 下面尝试用C#能不能画一下; 先在网上找一个画坐标的C#类,它的效果如下; 自己再增加一个函数,可以绘制中心在…...
【STC89C51单片机】定时器/计数器的理解
目录 定时器/计数器1. 定时器怎么定时简单理解(加1经过了多少时间)什么是时钟周期什么是机器周期 2.如何设置定时基本结构相关寄存器1. TMOD寄存器2. TCON寄存器 代码示例 定时器/计数器 STC89C51单片机的定时器和计数器(Timers and Counter…...
数据建模标准-关系建模
数据模型定义:DAMA数据治理体系中将数据模型定义为一种文档形式,数据模型是用来将数据需求从业务传递到IT,以及在IT内部从分析师、建模师和架构师到数据库设计人员和开发人员的主要媒介; 作用:记录数据需求和建模过程中产生的数据…...
福州做网站设计公司/十大电商代运营公司
引言:“软件定义汽车”的火热带动了工程师们对于汽车电子软件热烈地讨论。不曾想到,隐藏在控制器内部,默默地发挥着作用的汽车电子软件,如今备受瞩目。本人毕业到现在,一直在汽车行业做软件,切身感受到一系…...
加入google广告wordpress/学生个人网页制作成品
在当下这个互联网高速发展时代,很多企业时常会面临软件需求的增加以及开发人员的短缺等问题,所以才得以让低代码开发平台的迅速发展。而且根据今年的状况来看,国内很多企业对于低代码的市场需求也在逐步增加。面对这样的局面,会给…...
网站设置5个关键词/百家号查询排名数据查询
系统信息 arch 显示机器的处理器架构 uname -m 显示机器的处理器架构 uname -r 显示正在使用的内核版本 dmidecode -q 显示硬件系统部件 - (SMBIOS / DMI) hdparm -i /dev/hda 罗列一个磁盘的架构特性 hdparm -tT /dev/sda 在磁盘上执行测试性读取操作 cat /proc/cpuinfo …...
wordpress自带相册/营销策略从哪几个方面分析
目录 前言 一、词向量基础 1.单词的表示 2.从独热编码到分布式表示 3.词向量的训练 二、SkipGram模型详解 1.训练词向量的核心思想 2.SkipGram的目标函数 3.SkipGram的负采样 三、其他词向量技术 1.矩阵分解法 2.Glove向量 总结 前言 上一章已经介绍完自然语言处…...
平台网站可以做第三方检测报告/淘宝seo 优化软件
首先我们这里提到的锁,是把所需要的代码块,资源,或数据锁上,在操作他们的时候只允许一个线程去做操作。最终结果是为了保证cpu计算结果的正确性。 对不可重入锁的理解: public class Test{Lock lock new Lock();pub…...
备案 网站首页url/磁力岛引擎
监控对于我们来说到底有多重要? 网络监控是企业IT运维中必不可少的重要一环,传统的监控工具以监控各个服务的健康和性能为中心。而随着数字化时代的到来,我们需要一个更加全面的监控视图,能够把不同环境下所有资源间的流量信息进行…...