【Linux】基于单例模式的线程池设计
📝前言:
这篇文章我们来讲讲Linux——基于单例模式的线程池设计
🎬个人简介:努力学习ing
📋个人专栏:Linux
🎀CSDN主页 愚润求学
🌄其他专栏:C++学习笔记,C语言入门基础,python入门基础,C++刷题专栏
这里写目录标题
- 一,设计框架
- 1. 总体框架
- 2. 线程池模块
- 2.1 什么是线程池
- 2.2 设计思路
- 3. 什么是单例模式
- 3.1 饿汉模式
- 3.2 懒汉模式(更推荐)
- 二,实现代码
- 三,测试结果
- 四,线程安全和重入问题
- 五、锁的其他问题
- 1. 死锁问题
- 2. 死锁四个必要条件
- 3. 避免死锁
- 六、STL和智能指针的线程安全
一,设计框架
1. 总体框架
我们需要的模块
- 日志
Mylog
:用于DEBUG - 线程
MyThread
:用于生成多个线程 - 线程池
ThreadPool
,利用前两个模块来实现一个线程池
2. 线程池模块
2.1 什么是线程池
线程池 是一种多线程处理模式,属于池化技术的一种。它通过提前创建并管理一定数量的线程,重复利用这些线程处理任务,避免了频繁创建和销毁线程的开销,从而提升系统性能和资源利用率
2.2 设计思路
- 我们用一个
queue
来存储待处理的任务 - 用一个
vector
来存储线程
3. 什么是单例模式
某些类, 只应该具有⼀个对象(实例), 就称之为单例
- 通常把构造函数,拷贝,赋值…设置成私有,或者直接禁用(在外部无法创建实例)
- 只保留一个
static
实例成员,这样类在被加载的时候,这个实例就存在了。(static
成员不属于任何一个对象,而属于类,只有一份)- 注意
static
成员的初始化是在类外的(但是属于类的一部分,可以访问类的私有成员)
- 注意
- 然后提供一个
public
的接口来返回这个实例给外部使用
3.1 饿汉模式
- 加载类的时候,直接创建整个
static
实例成员 - 问题:如果静态实例的体积大,则加载时间长
3.2 懒汉模式(更推荐)
- 延迟加载,我只定义静态成员实例的指针。
- 当真正使用到这个成员时,再创建并赋值给指针(这样可以提高内存使用率)
- 即:在
GetInstance
时发现指针为空再创建
- 即:在
可是还有几个设计上的问题:
- 没有实例,我们怎么调
GetInstance
创造实例?- 所以
GetInstance
也是Static
的
- 所以
- 获取实例不是线程安全的,要加锁(不然可能创建多个实例)
- 这个锁是什么锁?也需要是
static
的锁,因为没实例的时候拿不到类的锁。
- 这个锁是什么锁?也需要是
二,实现代码
细节我就不讲了,感兴趣的可以看注释
#pragma once
#include "MyThread.hpp"
#include "Mylog.hpp"
#include <vector>
#include <queue>
#include "Task.hpp"
#include <mutex>
#include <condition_variable>// 先不考虑单例模式
using namespace std;
using namespace tr;template <typename T> // 任务类型
class ThreadPool
{
private:ThreadPool(int th_num = 3): _th_num(th_num),_isrunning(false),_wait_thread(0){}void HandlerTask() // 单个线程处理任务的方法{while (true){LOG(LogLevel::DEBUG) << GetThreadName() << " is running";_mutex.lock();while (_tasks_queue.empty() && _isrunning) // 条件变量一定要注意虚假唤醒{_wait_thread++;// LOG(LogLevel::DEBUG) << GetThreadName() << " is waiting";_cond.wait(_mutex);// LOG(LogLevel::DEBUG) << GetThreadName() << " is waked";_wait_thread--; // 如果等待被唤醒(唤醒是由别的地方唤醒的)}// 如果还有任务就先不退if (_tasks_queue.empty() && !_isrunning){_mutex.unlock(); // 尽早释放锁LOG(LogLevel::DEBUG) << GetThreadName() << " quit";break;}T t = _tasks_queue.front();_tasks_queue.pop();_mutex.unlock(); // 尽早释放锁LOG(LogLevel::DEBUG) << GetThreadName() << " 获得一个任务...";t(); // 处理任务}}void Start() // 启动线程池{if (!_isrunning){LOG(LogLevel::DEBUG) << "启动线程池...";_isrunning = true;// 必须要先开空间(因为扩容不是线程安全的)// 1. 创建 Mythread 对象// 2. 启动线程(线程开始执行HandlerTask)// 3. vector扩容(移动现有线程对象,原来的迭代器失效)// 如果 3. 的时候有线程在执行 HandlerTask,则原 this 指针会失效(导致段错误)// 以上为个人理解_threads.reserve(_th_num);// lock_guard<mutex> lock(_mutex); 不能这样做,又是为什么呢???for (int i = 0; i < _th_num; i++){// 用 HandlerTask(需要捕捉 this指针) 构造 Mythread 临时对象(隐式类型转换),_threads.emplace_back([this](){HandlerTask();});}}elseLOG(LogLevel::DEBUG) << "线程池已经启动, 请勿重复启动...";}// 禁用拷贝和赋值ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;ThreadPool(const ThreadPool<T> &) = delete;public:static ThreadPool<T> *GetInstance(){if (_instance == nullptr){_lock.lock();if (_instance == nullptr) // 双层 if 判断,只有一个线程能拿到锁进入这里,不会全部线程都通过这个判断{_instance = new ThreadPool<T>();_instance->Start(); // 启动线程池LOG(LogLevel::DEBUG) << " 创建线程池单例" ;return _instance;}}LOG(LogLevel::DEBUG) << " 获取线程池单例";return _instance;}void Stop() // 暂停(结束)线程池{lock_guard<mutex> lock(_mutex);if (!_isrunning)return;_isrunning = false;// 唤醒所有线程,让线程知道 _isrunning 已经为 false 了_cond.notify_all();LOG(LogLevel::INFO) << "线程池退出...";}void Wait() // 等待所有线程{for (auto &th : _threads){// LOG(LogLevel::INFO) << th.GetName() << " 退出...";th.Join();}}void Enqueue(const T &task) // 入队任务{lock_guard<mutex> lock(_mutex);if (_isrunning){_tasks_queue.push(task);// LOG(LogLevel::INFO) << "插入了一个任务";if (_wait_thread > 0)_cond.notify_one();}}~ThreadPool(){}private:bool _isrunning;queue<T> _tasks_queue;vector<Mythread> _threads;int _th_num; // 线程数量int _wait_thread;mutex _mutex; // 标准库的锁不用初始化condition_variable_any _cond;static ThreadPool<T> *_instance; // 静态成员实例static mutex _lock;
};template <typename T> // 模板类静态成员初始化必须带 template <typename T>, 因为模版要实例化
ThreadPool<T> *ThreadPool<T>::_instance = nullptr; // 还要声明模版的作用域
template <typename T>
mutex ThreadPool<T>::_lock;
主体代码为上面这部分的,如果想要获取其中其他模块的代码,可以访问我的GIthub
三,测试结果
四,线程安全和重入问题
线程安全(描述线程):就是多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果。一般而言,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量执行操作,并且没有锁保护的情况下,容易出现该问题
重入(描述函数):同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进⼊,我们称之为重⼊。一个函数在重⼊的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数
重入分两种:
- 多线程重入函数
- 信号导致⼀个执行流重复进入函数
线程安全不一定是可重入的,而可重入函数则⼀定是线程安全的。
五、锁的其他问题
1. 死锁问题
死锁是指在⼀组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站⽤不会释放的资源⽽处于的⼀种永久等待状态。
- 比如:一个公共资源的访问需要两把锁,但是现在线程 A 和线程 B各自占一个,并且都不愿意放,还不断申请对方的,导致两者都申请不到造成死锁
- 还比如,单线程死锁(自己带着锁,还自己把自己挂起了)
2. 死锁四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
3. 避免死锁
那就是破坏死锁的四个必要条件,使之不满足…
有避免死锁的算法:银行家算法、死锁检测算法(这里不做介绍)
六、STL和智能指针的线程安全
- STL 容器,不是线程安全的,比如扩容等操作,是可能被切换的
- 智能指针本身是线程安全的,但是它们所指向的资源不是线程安全的
🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!