当前位置: 首页 > news >正文

深入了解linux系统—— 线程池

线程池

通过之前的学习,了解了Linux线程相关知识,我们可以创建新线程,让新线程去调用函数;

这样我们就可以给新线程分配任务,让新线程去执行,主线程等待

这里如果有任务,再去创建线程,让线程执行,这就会导致多次调用系统调用创建线程,进而影响缓冲局部性和整体性能。

线程池,是一种线程使用模式,通过预先创建线程来减少系统调用的次数。

线程池,和之前实现的进程池一样,预先创建并维护多个线程,这些线程等待分配任务(阻塞等待);当有任务时,唤醒其中的一些线程去执行这些任务;

这样不仅能保证内核的充分利用,还能防止过分调度

线程池使用场景:

  • 需要大量线程来完成任务,且完成任务的时间比较短;例如WEB负服务器完成网页请求。
  • 对性能要求苛刻的应用;例如要求服务器迅速响应客户请求。
  • 接受突发性的大量请求,但不至于服务器因此产生大量线程的应用。

对于线程池,可以简单分为两种:

  1. 固定数量线程池:创建固定数量的线程,循环从人物队列中获取任务对象,获取到任务对象之后执行任务对象中的接口。
  2. 浮动线程池,线程的数量可以根据任务对象数量动态调整。

在这里插入图片描述

设计与实现

简单了解了线程池,现在来设计一个创建固定线程个数的线程池。(这里面默认设置线程数量为5

首先,线程池肯定要有线程吧,这里就复用之前是封装好的Thread,使用vector将这些线程存储起来。

其次,也要有任务,这里就使用queue来存储这些任务;

然后,为了保证任务队列中没有任务时,线程要阻塞等待;任务队列中有任务时,要有线程去处理任务;就要存在互斥信号量mutex以及条件变量cond(这里使用之前封装好的MutexCond

最后,在线程池中,我们也可以使用记录一下线程的数量,以及线程的运行状态。

    template <typename T>class Threadpool{private:std::vector<Thread> _threadpool; // 线程std::queue<T> _taskqueue;        // 任务队列Mutex _mutex;                    // 互斥量Cond _cond;                      // 条件变量int _num;                        // 线程数量bool _runing;                    // 运行状态};

1. 构造与析构函数

对于一个线程池,创建线程池对象时,要初始化其中的每一个线程;

要初始换线程,就要存在一个func_t类型的函数(using func_t = std::function<void()>;),这里我们可以使用lambda表达式,其中去调用Threadpool的类内方法HandlerTask

对于析构函数,我们要回收线程池中所有的线程;

这里就可以实现一个Join方法,在使用时就可以调用Join回收线程池中的线程;而在析构函数中也要调用Join方法。

        Threadpool(int num = default_num) : _num(num), _runing(false){for (int i = 0; i < num; i++){_threadpool.emplace_back([this](){HandlerTask();})}}void HandlerTask(){// 线程执行函数...}void Join(){for(auto& thread : _threadpool){thread.Join();}}~Threadpool(){Join();}

2. 运行线程池

这里我们复用Thread的代码,在创建出线程池对象时,其实并没有创建线程,线程池也并没有运行起来;

这里就设计实现一个Start方法,来启动线程池(创建线程),让线程池运行起来;并且实现HandlerTask方法(存在BUG

启动线程池

实现Start方法很简单,只需要一次调用每一个线程的Start方法,创建线程即可。

实现HandlerTask

HandlerTask方法,线程池所创建的每一个线程都要执行方法;

所以该方法就要实现线程从线程池中取任务,并且执行该任务。

  • 如果线程池任务队列中存在任务,线程取出并执行该任务。
  • 如果线程池任务队列没有任务,线程就要阻塞等待。

注意:任务队列对于多线程来说,就是临界资源;为了保证数据一致,线程在访问时就要现申请临界资源(申请失败也要阻塞等待)

        void HandlerTask(){// 线程执行函数...while (true){LockGroup lgp(_mutex);{while (_taskqueue.empty()){_cond.Wait(_mutex.GetMutex());}// 取任务T task = _taskqueue.front();_taskqueue.pop();}// 处理任务sleep(1);}}void Start(){_runing = true;for(auto& thread : _threadpool){thread.Start(); }}

到这里,实现的线程池已经可以运行起来了;这里测试由于没有像任务队列中存放任务,所有的线程都要阻塞等待。

int main()
{Threadpool<int> tpl;tpl.Start();sleep(10);return 0;
}

在这里插入图片描述

3. 终止线程池

上述代码,线程池已经可以运行起来了,但是,存在一个BUG

经过测试也不难发现,在程序sleep10秒后,程序要退出,调用Threadpool析构函数,主线程等待并回收新线程;

但是,此时所有的新线程都处于阻塞等待状态,主线程要阻塞等待新线程退出;所以就导致所有的线程都在阻塞等待。

所以,这里我们就要实现一个方法,来终止线程池

首先,终止线程池,将线程池的运行状态设置成false

但是,将线程设置成false之后,其他线程有的可能在执行任务,有的可能在阻塞等待;难道线程池都结束了,这些等待中的线程还用阻塞等待着吗?

所以,将线程池状态设置成false之后,要唤醒所有正在等待的线程

唤醒所有等待的线程之后,问题又来了:

  • 如果此时任务队列中没有任务,这些线程就会再次进入循环,阻塞等待。
  • 如果此时任务队列中存在任务,这些线程是直接退出,还是继续执行任务?

对于第一个问题,这里就需要修改上面的HandlerTask函数,至于如何修改,先看第二个问题;

如果线程池终止了,此时线程池还存在任务,这里就要线程继续执行任务,直到将任务队列中的任务执行完。

所以,线程要阻塞等待的条件不再是 任务队列为空(_taskqueue.empty());而是任务队列为空,并且当前线程池正在运行(_taskqueue.empty() && _runing)

而线程阻塞等待的条件变了,那线程在取任务之前,任务队列就可能为空(线程池运行状态为false,任务队列为空);

所以,在先去从任务队列中取任务之前,要先判断任务队列是否为空;如果为空,当前线程池就结束了,直接break即可;(break后,函数结束,线程也就结束了)如果不为空,才取任务执行任务。

所以,要实现的StopWeakUp和修改后的HandlerTask

    static int default_num = 5;template <typename T>class Threadpool{void Weakup(){LOG(Level::DEBUG) << "唤醒所有线程";_cond.Broadcast();}public:void HandlerTask(){while (true){LockGroup lgp(_mutex);{while (_taskqueue.empty() && _runing){_cond.Wait(_mutex.GetMutex());}// 取任务if (_taskqueue.empty()){LOG(Level::DEBUG) << "线程退出";break;}T task = _taskqueue.front();_taskqueue.pop();}// 处理任务sleep(1);}}void Stop(){_runing = false;Weakup();}private:std::vector<Thread> _threadpool; // 线程std::queue<T> _taskqueue;        // 任务队列Mutex _mutex;                    // 互斥量Cond _cond;                      // 条件变量int _num;                        // 线程数量bool _runing;                    // 运行状态};

这里实现Weakup方法,不希望被调用,只在内部使用,就设置成私有。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

单例模式

什么是单例模式呢?

就好比现实生活中,一个人只能存在一个合法夫妻一样;

在上述实现的线程池代码中,可以发现线程池整体还是很大的;如果存在多个线程池对象,是十分消耗内存资源的。

在很多服务器开发场景中,经常要让服务器加载很多数据,这是就需要一个单例的来管理这些数据。

简单来说,单例模式就是在内存中只能存在一个对象。

1. 饿汉模式

对于饿汉实现,抽象来说就是:

吃完饭,立即洗碗,在每次吃饭时都可以直接吃。

我们就可以理解为,先创建对象,这样在使用时就可以直接调用。

template <typename T>
class Singleton
{static T data;public:static T *GetInstance(){return &data;}
};

这样每次在使用时,通过类直接调用静态成员函数。

此外,单例模式只允许存在一个对象,所以拷贝构造,拷贝赋值等都要私有化private或者删除delete

2. 懒汉模式

对于懒汉模式,抽象来说:吃完饭,先不洗碗,在下次吃饭前再洗。

我们就可以理解为,先不创建对象,在要使用时再创建对象。

这里懒汉实现方式核心思想就是 :延时加载,在使用时在创建对象,优化服务器是启动速度

template <typename T>
class Singleton
{static T *inst;public:static T *GetInstance(){if (inst == NULL){inst = new T();}return inst;}
};

3. 实现懒汉单例模式

对于懒汉模式,存在一个问题:线程不安全

在第一次调用GetInstance时,多线程同时访问GetInstance时,就会创建出多个对象

这里就要对其进行加锁

template <typename T>
class Singleton
{volatile static T *inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.static std::mutex lock;public:static T *GetInstance(){if (inst == NULL){                // 双重判定空指针, 降低锁冲突的概率, 提⾼性能.lock.lock(); // 使⽤互斥锁, 保证多线程情况下也只调⽤⼀次 new.if (inst == NULL){inst = new T();}lock.unlock();}return inst;}
};

这样实现的单例模式,使用起来也太不方便了,这里将其设计到线程池内部方便使用;

class Threadpool // 注:类名这里应该是 ThreadPool<T>(代码笔误,否则模板无法生效)
{
public:
static ThreadPool<T> *GetInstance()
{// 第一次检查:未加锁,快速判断实例是否已存在(避免每次调用都加锁,提高效率)if (inc == nullptr){// 加锁:确保多线程下只有一个线程能进入“创建实例”的逻辑LockGuard lockguard(_lock);LOG(LogLevel::DEBUG) << "获取单例....";// 第二次检查:加锁后再次判断(防止多线程同时通过第一次检查,重复创建实例)if (inc == nullptr){LOG(LogLevel::DEBUG) << "首次使用单例, 创建之....";inc = new ThreadPool<T>();  // 创建线程池实例inc->Start();  // 启动线程池(初始化线程、开始接收任务等,具体逻辑在 Start() 中)}}return inc;  // 返回唯一实例指针
}private:// 静态成员:单例实例指针 + 互斥锁(保证线程安全)static ThreadPool<T> *inc;static Mutex _lock; 
};// 静态成员的类外初始化(模板类必须在类外显式初始化静态成员)
template <typename T>
ThreadPool<T> *ThreadPool<T>::inc = nullptr;

线程安全和重入

  1. 线程安全:多线程同时访问代码 / 共享资源时,结果与单线程执行一致,无数据损坏或逻辑混乱。核心解决 “共享资源竞争”,常用互斥锁、原子操作、无共享设计实现。
  2. 重入问题:函数被中断后重新进入(如中断 / 嵌套调用)仍能正确执行。关键看是否依赖静态 / 全局变量(依赖则不可重入,仅用局部变量通常可重入)。
  3. 关系:可重入≠线程安全(可重入函数多线程调用仍可能因共享资源不安全),线程安全≠可重入(线程安全函数若加锁,重入可能死锁);理想状态是 “可重入且线程安全”(如纯函数)。

文章转载自:

http://lWkuW4g5.tsLfz.cn
http://eER6afBi.tsLfz.cn
http://8W1RV75A.tsLfz.cn
http://Aa9c0laF.tsLfz.cn
http://9SeEKYQg.tsLfz.cn
http://cUIBLbuX.tsLfz.cn
http://YE4sbhMm.tsLfz.cn
http://dny3Ob4i.tsLfz.cn
http://e9JQNzI4.tsLfz.cn
http://Au0Fz3D6.tsLfz.cn
http://pyFUse8o.tsLfz.cn
http://DFYqFspk.tsLfz.cn
http://TlDSGsVg.tsLfz.cn
http://4xRTYfI7.tsLfz.cn
http://XbZ9IIQU.tsLfz.cn
http://1H72gL5y.tsLfz.cn
http://M3ip5V0w.tsLfz.cn
http://CzoQJHLM.tsLfz.cn
http://uOsOLwof.tsLfz.cn
http://QtjdL1gF.tsLfz.cn
http://6rwXgkPP.tsLfz.cn
http://YTvQFhr3.tsLfz.cn
http://Xdm3myrq.tsLfz.cn
http://uB6eChwY.tsLfz.cn
http://agkSArfj.tsLfz.cn
http://TbEmXMb0.tsLfz.cn
http://sbmU7BED.tsLfz.cn
http://0fjmV9eO.tsLfz.cn
http://dsCwBrMS.tsLfz.cn
http://Ch3nO9st.tsLfz.cn
http://www.dtcms.com/a/378224.html

相关文章:

  • 视频理解新纪元!VideoChat双模架构突破视频对话瓶颈,开启多模态交互智能时代
  • 【115】基于51单片机GSM防火防盗报警系统【Proteus仿真+Keil程序+报告+原理图】
  • 传统模型RNN与CNN介绍
  • 分布式专题——10.1 ShardingSphere介绍
  • 视频版权保护有哪些好用的加密方案
  • Rust 开发环境安装与 crates.io 国内源配置(Windows / macOS / Linux 全流程)
  • 前端全链路质量监控体系建设与实践分享
  • 使用python脚本储存mosquito服务器数据到sqlite
  • win10使用ssh访问vmware虚拟机
  • 高并发服务器-多路IO转接-select
  • 【WRF-VPRM 预处理器】HEG 安装(服务器)-MRT工具替代
  • 你知道服务器和电脑主机的区别吗?
  • 接力邓承浩,姜海荣能讲好深蓝汽车新故事吗?
  • 广东充电芯片助力新能源汽车车载系统升级
  • 大数据电商流量分析项目实战:Day2-1 补充Mysql和sql安装和学习
  • 【Unity UGUI 交互组件——Dropdown(TMP版本)(10)】
  • 自动化拨号爬虫体系:虚拟机集群部署与增量管理
  • 【机器人运动学】正运动学分析
  • 基于机器学习的P2P网贷平台信用违约预测模型
  • 工厂怎么认证iso14067
  • flutter项目 -- 换logo、名称 、签名、打包
  • 【Windows】VMware安装Ubuntu操作系统
  • 仿函数的分析与应用
  • 框架漏洞详解
  • Day02 集合 | 30. 串联所有单词的子串、146. LRU 缓存、811. 子域名访问计数
  • 基于springboot的教育资源共享管理系统
  • 汽车网络安全 CyberSecurity ISO/SAE 21434 测试之一
  • Fiddler
  • 【软件设计师(中级)】P1 计算机系统知识(待完成)
  • KronosTokenizer结构解析