【Linux】线程实例 | 简单线程池
今天来写一个简单版本的线程池
1.啥是线程池
池塘,顾名思义,线程池就是一个有很多线程的容器。
我们只需要把任务交到这个线程的池子里面,其就能帮我们多线程执行任务,计算出结果。
与阻塞队列不同的是,线程池中内有一个队列用于任务管理,并帮我们封装了线程创建的工作。我们不再需要在主执行流里面创建线程(创建线程也是有时间消耗的),而是只关注于任务的创建,交给线程池来运行并产生结果就OK了
前面已经学习过阻塞队列了,此时再来写线程池,就没有那么困难了!
本次线程池的设计还会采用单例模式,同一个模板类型的任务,只需要一个线程池即可
1.1 简单复习单例模式
单例模式分为两种设计方式,一个是懒汉,一个是饿汉
- 懒汉:刚开始先不创建单例,等第一次使用的时候在创建;缺点是第一次获取单例需要等待,优点是程序启动快
- 饿汉:main函数执行前,就将单例创建起来;缺点是程序启动会比较慢,优点是启动之后获取单例会快
2.代码示例-处理task
2.1 成员变量
因为是线程池,需要在内部创建出线程来运行,所以我们需要一个num来标识需要创建的线程的数量
template <class T>
class ThreadPool{
private:bool _isStart; // 线程池子是否启动int _threadNum; // 线程数量queue<T> _tq; // 任务队列pthread_mutex_t _mutex;// 锁pthread_cond_t _cond; // 条件变量static ThreadPool<T> *instance; // 单例模式需要用到的指针
}
这里我们并不需要弄一个数组来存放已经创建的线程,因为我们并不关心线程的退出信息,也不需要对线程进行管理。在创建好线程之后,直接detach
即可
static变量我们需要在类外初始化,因为是模板类型,所以还需要带上template
关键字
// 初始化static变量
template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;
2.2 构造/析构
本次使用的是懒汉模式的单例,提供一个指针作为单例,不开放构造函数
private:ThreadPool(int num = DEFALUT_NUM): _threadNum(num),_isStart(false){assert(num > 0);pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);}ThreadPool(const ThreadPool<T> &) = delete;//取消拷贝void operator=(const ThreadPool<T> &) = delete;//取消赋值
同时,利用delete
关键字,禁止拷贝构造和赋值重载;析构依旧保持公有
~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}
这种情况下,我们还需要有一个static成员函数来获取单例;在之前的单例模式博客中,提到当初实现的懒汉模式是线程不安全的,因为没有对线程进行加锁,避免多个执行流同时获取单例,导致单例对象冲突的问题。
现在学习了linux
的加锁操作,就可以避免掉这个bug了
两次nullptr判断
其中关于两次nullptr
判断的原因,详见注释
- 第一个判断是为了保证单例,只要单例存在了,就不再创建单例
- 第二个判断是保证线程安全,可能会出现线程a在创建单例,线程b在锁中等待的情况;此时如果不进行第二次nullptr判断,线程b从锁中被唤醒后,又会继续执行,多创建了一个单例!
public:static ThreadPool<T> *getInstance(){static pthread_mutex_t mt;//使用static,只会创建一次;避免多次实例化,一个执行流一个锁,失去效果pthread_mutex_init(&mt,nullptr);if (instance == nullptr) // 第一次判断{pthread_mutex_lock(&mt);// 加锁,保证只有一个执行流走到这里if (instance == nullptr)// 第二次判断是来确认的,避免出现在加锁前,被其他执行流获取过实例了{instance = new ThreadPool<T>();// 确认是null,创建单例}}pthread_mutex_unlock(&mt);pthread_mutex_destroy(&mt);return instance;}
2.3 启动线程池
有了线程池,接下来要做的就是启动它😁
启动之前,我们需要assert
判断一下该线程池是否已经启动了,避免多次启动线程池出现问题。启动完成之后,更新isStart
的状态值
void start(){assert(!_isStart);//如果开启了,那么就不能执行该函数for (int i = 0; i < _threadNum; i++){pthread_t temp;pthread_create(&temp, nullptr, threadRoutine, this);//把this当参数传入usleep(100);pthread_detach(temp);//分离线程}_isStart = true;//标识状态,代表线程池已经启动了}
这里还有另外一个函数threadRoutine
,这是每一个线程需要执行的函数,其为static函数。这里我们获取到的都是单例的this
指针,访问成员都需要通过this指针来访问
static void *threadRoutine(void *args)
{ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);//c++强转while (1){tp->lockQueue();while (!tp->haveTask()){tp->waitForTask();}// 任务被拿到了线程的上下文中T t = tp->pop();tp->unlockQueue();// 规定每一个封装的task对象都需要有一个run函数t.resultPrint(t.run());//运行并打印结果}
}
2.4 封装的加锁/解锁/通知操作
这部分操作比较简单,就不多提了。其实就是把已有的函数改个名字,变成无参可直接调用的函数罢了。
private:void lockQueue() { pthread_mutex_lock(&_mutex); }void unlockQueue() { pthread_mutex_unlock(&_mutex); }bool haveTask() { return !_tq.empty(); }void waitForTask() { pthread_cond_wait(&_cond, &_mutex); }void singalThread() { pthread_cond_signal(&_cond); }T pop(){T temp = _tq.front();_tq.pop();return temp;}
其中pop()
函数设置为了私有,因为线程池会自己开始处理任务,所以不需要外部pop
2.5 插入任务
最后就只剩下任务的插入了,插入一个任务后,使用条件变量,唤醒线程池中的一个线程来执行这个任务!
//往线程池中给任务void push(const T &in){lockQueue();_tq.push(in);//插入任务singalThread();//任务插入后,唤醒一个线程来执行unlockQueue();}
到这里,线程池就大功告成了!
3.测试
本次测试依旧使用了在线程博客中提到过的task.hpp
,完整代码详见我的gitee仓库
因为使用了线程池,主执行流只需要来派发任务即可;
#include "threadpool.hpp"
#include "task.hpp"
#include <string>
#include <time.h>int main()
{const string operators = "+/*/%";ThreadPool<Task>*tp = ThreadPool<Task>::getInstance();tp->start();srand((unsigned long)time(nullptr) ^ getpid() ^ pthread_self());// 派发任务的线程while(1){int one = rand()%50;int two = rand()%10;char oper = operators[rand()%operators.size()];cout << "[" << pthread_self() << "] 主线程派发计算任务: " << one << oper << two << "=?" << "\n";Task t(one, two, oper);tp->push(t);sleep(1);}}
此时线程池就会帮我们运行,并将结果输出!
[muxue@bt-7274:~/git/linux/code/23-01-18 threadpool]$ ./test
[140202992179008] 主线程派发计算任务: 14/8=?
[140202973767424] 新线程完成计算任务: 14/8=1
[140202992179008] 主线程派发计算任务: 43*2=?
[140202965374720] 新线程完成计算任务: 43*2=86
[140202992179008] 主线程派发计算任务: 10/9=?
[140202956982016] 新线程完成计算任务: 10/9=1
[140202992179008] 主线程派发计算任务: 25*9=?
[140202948589312] 新线程完成计算任务: 25*9=225
[140202992179008] 主线程派发计算任务: 8/0=?
div zero, abort
[140202940196608] 新线程完成计算任务: 8/0=-1
[140202992179008] 主线程派发计算任务: 38%1=?
[140202973767424] 新线程完成计算任务: 38%1=0
[140202992179008] 主线程派发计算任务: 23/7=?
[140202965374720] 新线程完成计算任务: 23/7=3
[140202992179008] 主线程派发计算任务: 4%4=?
[140202956982016] 新线程完成计算任务: 4%4=0
[140202992179008] 主线程派发计算任务: 44*8=?
[140202948589312] 新线程完成计算任务: 44*8=352
[140202992179008] 主线程派发计算任务: 4/2=?
3.1 修改轻量级进程的名字
Linux提供了一个有趣的接口,可以允许我们修改轻量级进程的名字;
没有修改的时候,默认的名字都是该进程的可执行程序的名字
[muxue@bt-7274:~/git/linux/code/23-01-18 threadpool]$ ps -aLPID LWP TTY TIME CMD6592 6592 pts/7 00:00:00 test6592 6593 pts/7 00:00:00 test6592 6594 pts/7 00:00:00 test6592 6595 pts/7 00:00:00 test6592 6596 pts/7 00:00:00 test6592 6597 pts/7 00:00:00 test6730 6730 pts/8 00:00:00 ps
我们使用prctl
接口,修改名字;这个接口的作用是对一个进程进行操作。
#include <sys/prctl.h>
int prctl(int option, unsigned long arg2, unsigned long arg3,unsigned long arg4, unsigned long arg5);
其中修改线程名字的操作如下
prctl(PR_SET_NAME, "handler");//修改线程名字为handler
分别修改主执行流和线程池中线程的名字,即可获得不一样的结果
[muxue@bt-7274:~/git/linux/code/23-01-18 threadpool]$ ps -aLPID LWP TTY TIME CMD7793 7793 pts/7 00:00:00 master7793 7794 pts/7 00:00:00 handler7793 7795 pts/7 00:00:00 handler7793 7796 pts/7 00:00:00 handler7793 7797 pts/7 00:00:00 handler7793 7798 pts/7 00:00:00 handler7828 7828 pts/8 00:00:00 ps
这样可以用于标识线程的属性,还是有些用的!
The end
本篇博客到这里就over啦,有啥问题欢迎评论区提出哦!
相关文章:

【Linux】线程实例 | 简单线程池
今天来写一个简单版本的线程池 1.啥是线程池 池塘,顾名思义,线程池就是一个有很多线程的容器。 我们只需要把任务交到这个线程的池子里面,其就能帮我们多线程执行任务,计算出结果。 与阻塞队列不同的是,线程池中内有…...

ATAC-seq 数据分析实战
文章目录一、 ATAC-seq原理和基础知识1. ATAC-seq原理2. Tn5转座子1. 转座概念2. 参与分子1. 转座子(1) 简化的转座子结构(2) Tn5转座子的结构2. 转座酶3. 转座过程二、数据比对和过滤一、 ATAC-seq原理和基础知识 1. ATAC-seq原…...

设计模式-第13章(状态模式)
状态模式状态模式状态模式的好处和用处工作状态状态模式 状态模式(State),当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。 状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况…...

ReentrantLock源码分析(一)加锁流程分析
一、ReetrantLock的使用示例 static ReentrantLock lock new ReentrantLock(); public static void main(String[] args) throws InterruptedException { new Thread(ClassLayOutTest::reentrantLockDemo, "threadA").start(); Thread.sleep(1000);…...

【C++】list的模拟实现
文章目录1.list 底层2. list的模拟实现1. list_node 类设计2. list类如何调用类型3 .push_back(正常实现)4. 迭代器的实现第一个模板参数Tconst迭代器第二个模板参数Ref第三个模板参数Ptr对list封装的理解5. insert6.push_back与 push_front(复用)7. erase8. pop_back与pop_fro…...
Python连接es笔记三之es更新操作
这一篇笔记介绍如何使用 Python 对数据进行更新操作。 对于 es 的更新的操作,不用到 Search() 方法,而是直接使用 es 的连接加上相应的函数来操作,本篇笔记目录如下: 获取连接update()update_by_query()批量更新UpdateByQuery()…...

哪个牌子的蓝牙耳机音质好?音质比较好的蓝牙耳机排名
蓝牙耳机经过多年发展,无论是在外观设计还是性能配置上都有很大的进步,越来越多的蓝牙耳机开始注重音质表现,逐渐有HIFI音质、无损音质出现在大众视野。那么哪个牌子的蓝牙耳机音质好?接下来,我来给大家分享几款音质比…...

Qt实用技巧:Qt中浮点数的相等比较方式(包括单精度和双精度)
若该文为原创文章,转载请注明原文出处 本文章博客地址:https://hpzwl.blog.csdn.net/article/details/129464152 红胖子(红模仿)的博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软…...
【数据结构初阶】双向循环链表
目录一.链表的分类二.与单链表相比三.实现增删查改1.双向循环链表结构的创建2.创建新节点3.初始化链表4.头插和尾插5.判断链表是否为空6.头删和尾删7.打印函数8.查找函数9.删除pos位置节点10.在pos前位置插入数据11.优化升级一.链表的分类 链表可有根据单向双向、有无哨兵位、…...
0104BeanDefinition合并和BeanClass加载-Bean生命周期详解-spring
文章目录1 前言2 BeanDefinition合并2.1 BeanDefinition合并在做什么?2.2 BeanDefinition怎么合并2.3 示例演示3 Bean Class 加载后记1 前言 下面要介绍的阶段,都是在调用getBean()从容器中获取bean对象的过程中发生的操作,我们需要更多的去…...

Java集合进阶(三)
文章目录一、Map1. 概述2. 基本功能3. 遍历4. 遍历学生对象5. 集合嵌套6. 统计字符出现次数二、Collections1. 常用方法2. 学生对象排序三、模拟斗地主一、Map 1. 概述 Interface Map<K, V>:K 是键的类型,V 是值的类型。 将键映射到值的对象&…...

【网络】什么是RPC?RPC与HTTP有什么关系?
文章目录RPC是什么RPC和HTTP的关系和区别[附]关于REST论文中提到的"HTTP不是RPC"重点参考 凤凰架构-远程过程调用 既然有HTTP为什么还要有RPC? RPC是什么 RPC(Remote Procedure Call):即远程过程调用,目的是为了让计算机能够跟调用…...

[手撕数据结构]栈的深入学习-java实现
CSDN的各位uu们你们好,今天千泽带来了栈的深入学习,我们会简单的用代码实现一下栈, 接下来让我们一起进入栈的神奇小世界吧!0.速览文章一、栈的定义1. 栈的概念2. 栈的图解二、栈的模拟实现三.栈的经典使用场景-逆波兰表达式总结一、栈的定义 1. 栈的概念 栈:一种…...

2.线性表的顺序表示
数据结构很重要! 数据结构很重要!!! 数据结构很重要!!!! 思考 1.线性表的顺序表示内容有哪些?(What) 2.为什么要学线性表的顺序表示? ? (Why)…...

eps文件删除了能恢复吗?恢复误删eps文件的三种方法
eps文件格式专为矢量图像和图形而设计。虽然没有被广泛使用,但它仍然受到各种插画家和平面设计师的钟爱。eps文件十分适合创建徽标和商标设计,主要应用见于广告牌、海报和横幅。可是在使用设备过程中,难免会遇到数据丢失问题,如果…...
【C++】运算符重载练习——Date 类
文章目录👉日期类介绍👈👉日期类实现👈📕 成员变量📕 构造函数📕 对应月份天数📕 赋值重载📕 比较运算符重载📕 计算 运算符重载👉源代码…...

Redis学习(13)之Lua脚本【环境准备】
文章目录一 Lua入门环境准备1.1 Lua简介1.2 Linux 系统安装Lua1.2.1 Lua 下载1.2.2 Lua 安装1.3 Hello World1.3.1 命令行模式1.3.2 脚本文件模式1.3.3 两种脚本运行方式1.4 Win安装Lua1.4.1 LuaForWindows的安装1.4.2 SciTE修改字体大小1.4.3 SciTE中文乱码1.4.4 SciTE快捷键工…...

关于BLE的一些知识总结
数据包长度对于BLE4.0/4.1来说,一个数据包的有效载荷最大为20字节对于BLE4.2以上,数据包的有效载荷扩大为251字节传输速率在不考虑跳频间隔的情况下,最大传输速率为:1)BLE4.0/4.1的理论吞吐率为39kb/s;2&am…...

Spring框架源码分析一
如何看源码(方法论)不要忽略源码中的注释使用翻译工具先梳理脉络,然后梳理细节即总分总,先总体过一遍,再看细节,再做一个总结大胆猜测(8分靠猜),小心验证,再调…...

CSS常用内容总结(扫盲)
文章目录前言相关概念【了解】脚本语言什么是脚本语言脚本语言有什么特点常见的脚本语言什么是动态语言,什么是静态语言动态语言和静态语言两者之间有何区别CSSCSS是什么CSS的特点一、CSS代码怎么写基本语法规则引入方式内部样式内联样式表外部样式代码风格二、CSS的…...
Java 语言特性(面试系列1)
一、面向对象编程 1. 封装(Encapsulation) 定义:将数据(属性)和操作数据的方法绑定在一起,通过访问控制符(private、protected、public)隐藏内部实现细节。示例: public …...
Oracle查询表空间大小
1 查询数据库中所有的表空间以及表空间所占空间的大小 SELECTtablespace_name,sum( bytes ) / 1024 / 1024 FROMdba_data_files GROUP BYtablespace_name; 2 Oracle查询表空间大小及每个表所占空间的大小 SELECTtablespace_name,file_id,file_name,round( bytes / ( 1024 …...

从零开始打造 OpenSTLinux 6.6 Yocto 系统(基于STM32CubeMX)(九)
设备树移植 和uboot设备树修改的内容同步到kernel将设备树stm32mp157d-stm32mp157daa1-mx.dts复制到内核源码目录下 源码修改及编译 修改arch/arm/boot/dts/st/Makefile,新增设备树编译 stm32mp157f-ev1-m4-examples.dtb \stm32mp157d-stm32mp157daa1-mx.dtb修改…...

BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...
【JavaSE】绘图与事件入门学习笔记
-Java绘图坐标体系 坐标体系-介绍 坐标原点位于左上角,以像素为单位。 在Java坐标系中,第一个是x坐标,表示当前位置为水平方向,距离坐标原点x个像素;第二个是y坐标,表示当前位置为垂直方向,距离坐标原点y个像素。 坐标体系-像素 …...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...

使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...
WebRTC从入门到实践 - 零基础教程
WebRTC从入门到实践 - 零基础教程 目录 WebRTC简介 基础概念 工作原理 开发环境搭建 基础实践 三个实战案例 常见问题解答 1. WebRTC简介 1.1 什么是WebRTC? WebRTC(Web Real-Time Communication)是一个支持网页浏览器进行实时语音…...

Ubuntu系统复制(U盘-电脑硬盘)
所需环境 电脑自带硬盘:1块 (1T) U盘1:Ubuntu系统引导盘(用于“U盘2”复制到“电脑自带硬盘”) U盘2:Ubuntu系统盘(1T,用于被复制) !!!建议“电脑…...
深度解析:etcd 在 Milvus 向量数据库中的关键作用
目录 🚀 深度解析:etcd 在 Milvus 向量数据库中的关键作用 💡 什么是 etcd? 🧠 Milvus 架构简介 📦 etcd 在 Milvus 中的核心作用 🔧 实际工作流程示意 ⚠️ 如果 etcd 出现问题会怎样&am…...