【Linux实战 】Linux 线程池的设计、实现与单例模式应用
前言:
在上文中我们实现了日志文件系统【Linux 实战】从0到1手搓日志系统:附完整代码-CSDN博客
本文我们再来看看线程池是如何实现的
线程池
线程池是预先创建一定数量可复用线程的集合,通过统一管理线程来执行任务,以减少线程频繁创建与销毁的开销,并控制并发线程数量,从而优化系统性能与资源利用率的机制。
设计思路
核心成员
同步工具:使用之前我们就封装好了的互斥锁Mutex、与条件变量Cond
线程池的封装:使用封装好的Thread、以及日志文件Log
使用STL容器:vector作为存储线程的容器、queue作为任务队列
线程池初始化
通过构造函数ThreadPool(int num)指定线程数量。
并在构造函数中创建num个线程对象,每个线程绑定一个任务处理函数:HandlerTask()
初始化完毕,等待Start()方法启动每个线程。
线程工作机制
每个线程都运行HandlerTask()函数,其HandlerTask()中的主要逻辑是:
1.线程启动后进入循环,通过互斥锁保护任务队列的访问。
2.当任务队列为空且线程池正常运行中,线程进入条件变量下等待。休眠计数器(_sleepnum)更新,用于后续的唤醒策略。
3.若不为空,或被唤醒则进行任务的获取以及处理。在锁外进行对任务的处理,避免长时间持有锁。
任务管理机制
任务通过Equeue()方法进入任务队列,类型为模板参数T。
添加任务时,使用互斥锁保证安全。
添加完任务后,如果全部线程都去休眠了,那么唤醒一个休眠线程,来处理任务。
线程池控制
Start():用于启动所有线程,并设置运行标志_isrunning = true。
Stop():设置运行标志位_isrunning = fasle,表示终止线程池的运行。
线程池的退出条件:需要将全部任务处理完后,才能退出。
Join():等待所有退出的线程。
同步与唤醒策略
使用互斥锁Mutex保护所有的共享资源。
使用条件变量Cond用于线程的休眠与唤醒。
替工两种唤醒方式:WakeUpOne()唤醒一个线程,WakeUpAll()唤醒全部线程。
线程退出时会检查双重条件:任务队列与运行状态,保证资源的正常释放。
关键技术点
模板的设计:通过模板参数T支持任务类型的任务,提高通用性。
线程安全:对所有的共享资源加锁进程保护。
线程的退出:确保任务队列为空 && 运行状态也为停止状态。
这种实现既保证了线程池的高效运行,又通过完善的同步机制和退出策略确保了线程安全和资源管理的可靠性。
完整代码
// 线程池的实现与封装
#include "Mutex.hpp"
#include "Thread.hpp"
#include "Log.hpp"
#include "Cond.hpp"
#include <vector>
#include <queue>namespace ThreadPoolModule
{using namespace MutexModule;using namespace LogModule;using namespace ThreadModule;using namespace CondModule;template <typename T>class ThreadPool{private:void WakeUpAll(){LockGuard lockgurad(_mutex);if (_sleepnum > 0){_cond.Broadcast();LOG(LogLevel::INFO) << "唤醒当前休眠的所有线程";}}void WakeUpOne(){_cond.Signal();LOG(LogLevel::INFO) << "唤醒当前休眠的一个线程";}public:ThreadPool(int num): _num(num), _isrunning(false), _sleepnum(0){for (int i = 0; i < num; i++){// Lambda表达式_threads.emplace_back([this](){ HandlerTask(); });}}void HandlerTask(){char name[128];// 获取对应线程的名字pthread_getname_np(pthread_self(), name, sizeof(name));while (true){T t;{LockGuard LockGuard(_mutex);// 1.任务队列为空 && 线程池正常运行while (_task.empty() && _isrunning){_sleepnum++;_cond.Wait(_mutex);_sleepnum--;}// 2.线程池终止 && 任务队列为空时;线程池才能退出if (!_isrunning && _task.empty()){LOG(LogLevel::INFO) << "线程池终止并退出";break;}// 这里一定有任务t = _task.front();_task.pop();}t(); // 锁外处理任务}}// 传入方法void Equeue(const T &in){LockGuard lockguard(_mutex);if (_isrunning){_task.push(in);// 当所有的线程都休眠了,需要唤醒线程if (_num == _sleepnum){WakeUpOne();}}}void Join(){for (auto &thread : _threads){thread.Join();}LOG(LogLevel::INFO) << "全部线程等待完毕";}void Stop(){if (!_isrunning)return;_isrunning = false;// 唤醒所有线程WakeUpAll();}void Start(){_isrunning = true;for (auto &thread : _threads){thread.Start();}}private:int _num; // 线程数量std::vector<Thread> _threads; // 线程std::queue<T> _task; // 任务队列Cond _cond;Mutex _mutex;bool _isrunning;int _sleepnum; // 休眠线程个数};
}
线程池的单例模式
单例模式:确保一个类在整个应用生命周期中只有 1 个实例,并提供一个全局唯一的访问点
单例模式是实现方式有两种:懒汉模式、饿汉模式。
饿汉模式:在对象创建之初就将全部资源加载好。
懒汉模式:将资源延迟加载,当需要使用对应资源时才进行加载,有助于减少启动的时间。
基本都使用懒汉模式设计
通过将构造函数私有化,并将拷贝构造、赋值重载禁用,在类中创建唯一的对象,来保证这个类有且仅有这一个对象!
完整代码
// 线程池的实现与封装
#include "Mutex.hpp"
#include "Thread.hpp"
#include "Log.hpp"
#include "Cond.hpp"
#include <vector>
#include <queue>// 在线程池基本实现的基础上实现线程池的单例模式
// 顾名思义,单例模式就是只运行该类创建一个对象
// 实现思路:1.将拷贝构造、复杂重载禁用 2.将构造函数私有化,并在类中实现对唯一对象的创建
// 3.使用懒汉方式实现:当需要使用时才去进行对应的创建namespace ThreadPoolModule
{using namespace MutexModule;using namespace LogModule;using namespace ThreadModule;using namespace CondModule;static const int gnum = 5;template <typename T>class ThreadPool{private:void WakeUpAll(){LockGuard lockgurad(_mutex);if (_sleepnum > 0){_cond.Broadcast();LOG(LogLevel::INFO) << "唤醒当前休眠的所有线程";}}void WakeUpOne(){_cond.Signal();LOG(LogLevel::INFO) << "唤醒当前休眠的一个线程";}// 私有化构造函数ThreadPool(int num = gnum): _num(num), _isrunning(false), _sleepnum(0){for (int i = 0; i < num; i++){// Lambda表达式_threads.emplace_back([this](){ HandlerTask(); });}}void HandlerTask(){char name[128];// 获取对应线程的名字pthread_getname_np(pthread_self(), name, sizeof(name));while (true){T t;{LockGuard LockGuard(_mutex);// 1.任务队列为空 && 线程池正常运行while (_task.empty() && _isrunning){_sleepnum++;_cond.Wait(_mutex);_sleepnum--;}// 2.线程池终止 && 任务队列为空时;线程池才能退出if (!_isrunning && _task.empty()){LOG(LogLevel::INFO) << "线程池终止并退出";break;}// 这里一定有任务t = _task.front();_task.pop();}t(); // 锁外处理任务}}void Start(){_isrunning = true;for (auto &thread : _threads){thread.Start();}}public:// 构建唯一的对象static ThreadPool<T> *GetObject() // 调用该函数时是没有对象的,而非静态函数必须要通过对象调用{if (_inc == nullptr){LOG(LogLevel::INFO) << "获取单例";LockGuard lockguard(_mutex_);// 由于静态成员函数无法访问非静态成员,所有_inc也必须是静态的if (_inc == nullptr){LOG(LogLevel::INFO) << "创建单例";_inc = new ThreadPool<T>();_inc->Start(); // 创建后启动线程池}}return _inc;}// 传入方法void Equeue(const T &in){LockGuard lockguard(_mutex);if (_isrunning){_task.push(in);// 当所有的线程都休眠了,需要唤醒线程if (_num == _sleepnum){WakeUpOne();}}}void Join(){for (auto &thread : _threads){thread.Join();}LOG(LogLevel::INFO) << "全部线程等待完毕";}void Stop(){if (!_isrunning)return;_isrunning = false;// 唤醒所有线程WakeUpAll();}private:int _num; // 线程数量std::vector<Thread> _threads; // 线程std::queue<T> _task; // 任务队列Cond _cond;Mutex _mutex;bool _isrunning;int _sleepnum; // 休眠线程个数static ThreadPool<T> *_inc; // 对象指针static Mutex _mutex_;};// 静态成员变量必须在类外初始化(必须要指定作用域)template <typename T>ThreadPool<T> *ThreadPool<T>::_inc = nullptr;template <typename T>Mutex ThreadPool<T>::_mutex_;
}