文章目录
- [9. 线程池](#9. 线程池)
-
- [9.1 日志与策略模式](#9.1 日志与策略模式)
- [9.2 线程安全的单例模式](#9.2 线程安全的单例模式)
-
- [9.2.1 什么是单例模式](#9.2.1 什么是单例模式)
- [9.2.2 单例模式的特点](#9.2.2 单例模式的特点)
- [9.2.3 饿汉实现方式和懒汉实现方式](#9.2.3 饿汉实现方式和懒汉实现方式)
- [9.2.4 饿汉方式实现单例模式](#9.2.4 饿汉方式实现单例模式)
- [9.2.5 懒汉方式实现单例模式](#9.2.5 懒汉方式实现单例模式)
- [9.2.6 懒汉方式实现单例模式(线程安全版本)](#9.2.6 懒汉方式实现单例模式(线程安全版本))
- [9.3 线程池设计](#9.3 线程池设计)
-
- [9.2.1 线程池](#9.2.1 线程池)
- [9.2.2 对线程做一个简单的封装-Thread.hpp](#9.2.2 对线程做一个简单的封装-Thread.hpp)
- [9.2.3 单例模式进程池](#9.2.3 单例模式进程池)
9. 线程池
9.1 日志与策略模式
什么是设计模式?
大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是设计模式。
日志认识:计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。
日志格式以下几个指标是必须得有的:
- 时间戳
- 日志等级
- 日志内容
以下几个指标是可选的:
- 文件名行号
- 进程,线程相关id信息等
日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx等等,我们依旧采用自定义日志的方式。
9.2 线程安全的单例模式
9.2.1 什么是单例模式
单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点
9.2.2 单例模式的特点
某些类, 只应该具有一个对象(实例), 就称之为单例。
例如一个男人只能有一个媳妇。
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中。此时往往要用一个单例的类来管理这些数据。
单例模式主要有两种实现方式:懒汉式和饿汉式。
9.2.3 饿汉实现方式和懒汉实现方式
洗碗的例子
- 吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
- 吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.
懒汉方式最核心的思想是 "延时加载". 从而能够优化服务器的启动速度。
9.2.4 饿汉方式实现单例模式
cpp
template <typename T>
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};
只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例。
9.2.5 懒汉方式实现单例模式
cpp
template <typename T>
class Singleton {
static T* inst;
public:
static T* GetInstance() {
if (inst == NULL) {
inst = new T();
}
return inst;
}
};
存在一个严重的问题, 线程不安全。
第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例。
但是后续再次调用, 就没有问题了
9.2.6 懒汉方式实现单例模式(线程安全版本)
cpp
// 懒汉模式, 线程安全
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;
}
};
注意事项:
- 加锁解锁的位置
- 双重 if 判定, 避免不必要的锁竞争
- volatile关键字防止过度优化
9.3 线程池设计
线程池:
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
线程池的应用场景:
- 需要大量的线程来完成任务,且完成任务的时间比较短。 比如WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。
线程池的种类
- 创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中的任务接口
- 浮动线程池,其他同上
此处,我们选择固定线程个数的线程池。
![](https://i-blog.csdnimg.cn/img_convert/c982a7066bebdf994d903d86c7659331.png)
9.2.1 线程池
ThreadPool.hpp
cpp
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>
// 线程信息结构体,保存线程ID和名称
struct ThreadInfo {
pthread_t tid; // 线程ID
std::string name; // 线程名称
};
// 默认线程池中线程数量
static const int defalutnum = 5;
template <class T>
class ThreadPool {
public:
// 互斥锁操作封装
void Lock() {
// 对互斥锁加锁,如果锁已被占用则阻塞等待
pthread_mutex_lock(&mutex_);
}
void Unlock() {
// 解锁互斥锁,释放锁的占用
pthread_mutex_unlock(&mutex_);
}
// 条件变量操作封装
// 唤醒一个正在等待该条件变量的线程
void Wakeup() {
// 唤醒一个正在等待该条件变量的线程
// 如果有多个线程等待,则随机唤醒一个
pthread_cond_signal(&cond_);//去任务队列唤醒
}
//线程休眠
void ThreadSleep() {
// 使当前线程在条件变量上等待
// 1. 自动释放互斥锁mutex_
// 2. 线程进入睡眠状态
// 3. 当被唤醒时,自动重新获取互斥锁
pthread_cond_wait(&cond_, &mutex_);
/*
参数和返回值:
参数1 cond:条件变量的指针
参数2 mutex:互斥锁的指针
返回值:成功返回0,失败返回错误码
*/
}
// 判断任务队列是否为空
bool IsQueueEmpty() {
// 返回任务队列的空状态
return tasks_.empty();
}
// 根据线程ID获取线程名称
std::string GetThreadName(pthread_t tid) {
// 遍历线程信息数组
for (const auto &ti : threads_) {
// 找到匹配的线程ID
if (ti.tid == tid)
return ti.name; // 返回对应的线程名
}
return "None"; // 未找到则返回"None"
}
public:
ThreadPool(int num = defalutnum) : threads_(num) {
pthread_mutex_init(&mutex_, nullptr); // 初始化互斥锁
/*
参数说明:
mutex: 指向 pthread_mutex_t 类型的指针,用于存储要初始化的互斥锁
attr: 指向 pthread_mutexattr_t 类型的指针,用于指定互斥锁的属性
传入 nullptr 表示使用默认属性
默认属性为:快速互斥锁(PTHREAD_MUTEX_FAST_NP)
返回值:
成功返回 0
失败返回错误码
*/
pthread_cond_init(&cond_, nullptr); // 初始化条件变量
/*
参数说明:
cond: 指向 pthread_cond_t 类型的指针,用于存储要初始化的条件变量
attr: 指向 pthread_condattr_t 类型的指针,用于指定条件变量的属性
传入 nullptr 表示使用默认属性
默认属性为:进程私有(PTHREAD_PROCESS_PRIVATE)
返回值:
成功返回 0
失败返回错误码
*/
}
// 1.启动线程池
void Start() {
// 获取线程数量(线程池大小)
int num = threads_.size();
// 循环创建工作线程
for (int i = 0; i < num; i++) {
// 为每个线程设置名称,格式为 "thread-1", "thread-2" 等
// std::to_string 是 C++ 标准库提供的函数,用于将数值类型转换为字符串
threads_[i].name = "thread-" + std::to_string(i + 1);
// 创建线程
pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
/*
int pthread_create(
pthread_t *thread, // 输出参数,存储新创建的线程ID
const pthread_attr_t *attr, // 线程属性,nullptr表示使用默认属性
void *(*start_routine)(void*), // 线程函数
void *arg // 传递给线程函数的参数
);
*/
}
}
// 工作线程的处理函数
static void* HandlerTask(void* args) {
// 将 void* 的参数args转换回线程池指针
ThreadPool<T>* tp = static_cast<ThreadPool<T>*>(args);
//static_cast<>用于进行编译时类型转换。
//语法: static_cast<新类型>(表达式)
//ThreadPool<T>*表示指向ThreadPool<T>类型对象的指针
// 获取当前线程的名称
// pthread_self() 返回当前线程ID
std::string name = tp->GetThreadName(pthread_self());
// 工作线程的主循环
while (true) {
// 先加锁:准备访问共享资源(任务队列)
tp->Lock();
// 当任务队列为空时,进入等待状态
while (tp->IsQueueEmpty()) {
tp->ThreadSleep(); // 条件变量等待
}
// 从任务队列取出一个任务
T t = tp->Pop();
// 取完任务后解锁:不再访问共享资源,让其他线程可以取任务
tp->Unlock();
// 执行任务,它调用了任务对象的 operator() 运算符,也就是实际执行任务的代码
t();//(不需要加锁,因为任务已经安全地取出)
// 输出任务执行结果
std::cout << name << " run, " << "result: " << t.GetResult() << std::endl;
}
}
// 向任务队列添加任务
void Push(const T& t) {
// 加锁保护任务队列
Lock();
// 将任务添加到队列末尾
tasks_.push(t);
Wakeup(); // 唤醒一个等待的工作线程来处理新任务,使用pthread_cond_signal通知
Unlock(); // 解锁,允许其他线程访问任务队列
}
// 从任务队列获取任务
T Pop() {
T t = tasks_.front();
tasks_.pop();
return t;
}
~ThreadPool() {
pthread_mutex_destroy(&mutex_); // 销毁互斥锁
pthread_cond_destroy(&cond_); // 销毁条件变量
}
private:
std::vector<ThreadInfo> threads_; // 工作线程集合
std::queue<T> tasks_; // 任务队列
pthread_mutex_t mutex_; // 任务队列互斥锁
pthread_cond_t cond_; // 任务队列条件变量
static ThreadPool<T>* tp_; // 单例指针
static pthread_mutex_t lock_; // 单例锁
};
// 静态成员初始化
template <class T>
ThreadPool<T>* ThreadPool<T>::tp_ = nullptr;
template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
Task.hpp
cpp
#pragma once
#include <iostream>
#include <string>
// 定义所有支持的运算符集合
std::string opers="+-*/%"; // 加、减、乘、除、取模
// 定义错误码枚举类型
enum {
DivZero=1, // 除零错误
ModZero, // 取模零错误(值为2)
Unknown // 未知错误(值为3)
};
// Task类:表示一个算术运算任务
class Task
{
public:
// 默认构造函数
Task()
{}
// 带参数的构造函数
// x: 第一个操作数
// y: 第二个操作数
// op: 运算符
Task(int x, int y, char op)
: data1_(x), // 初始化第一个操作数
data2_(y), // 初始化第二个操作数
oper_(op), // 初始化运算符
result_(0), // 初始化结果为0
exitcode_(0) // 初始化错误码为0(表示无错误)
{
}
// 执行运算的核心函数
void run()
{
switch (oper_)
{
case '+': // 加法运算
result_ = data1_ + data2_;
break;
case '-': // 减法运算
result_ = data1_ - data2_;
break;
case '*': // 乘法运算
result_ = data1_ * data2_;
break;
case '/': // 除法运算
{
if(data2_ == 0) exitcode_ = DivZero; // 检查除数为0的情况
//如果是0则设置错误代码为DivZero(除零错误)
else result_ = data1_ / data2_;
}
break;
case '%': // 取模运算
{
if(data2_ == 0) exitcode_ = ModZero; // 检查除数为0的情况
else result_ = data1_ % data2_;
}
break;
default: // 未知运算符
exitcode_ = Unknown;
break;
}
}
// 重载函数调用运算符"()",使对象可以像函数一样被调用
void operator ()()
{
run();
}
// 获取完整的运算结果字符串
// 格式:操作数1 运算符 操作数2 = 结果 [错误码]
std::string GetResult()
{
std::string r = std::to_string(data1_); // 转换第一个操作数
r += oper_; // 添加运算符
r += std::to_string(data2_); // 转换第二个操作数
r += "="; // 添加等号
r += std::to_string(result_); // 添加计算结果
r += "[code: "; // 添加错误码标记
r += std::to_string(exitcode_); // 添加错误码
r += "]";
return r;
}
// 获取任务的字符串表示(不包含结果)
// 格式:操作数1 运算符 操作数2 =?
std::string GetTask()
{
std::string r = std::to_string(data1_); // 转换第一个操作数
r += oper_; // 添加运算符
r += std::to_string(data2_); // 转换第二个操作数
r += "=?"; // 添加待计算标记
return r;
}
// 析构函数(目前为空)
~Task()
{
}
private:
int data1_; // 第一个操作数
int data2_; // 第二个操作数
char oper_; // 运算符
int result_; // 计算结果
int exitcode_; // 错误码(0表示无错误)
};
cpp
#include <iostream>
#include <ctime>
#include "ThreadPool.hpp" // 线程池头文件
#include "Task.hpp" // 任务类头文件
int main()
{
// 直接创建进程池
ThreadPool<Task> *tp = new ThreadPool<Task>(5); // 创建5个线程的线程池
tp->Start();
srand(time(nullptr) ^ getpid());
// 主循环:持续生成任务
while(true)
{
// 1. 构建任务
int x = rand() % 10 + 1; // 生成1-10的随机数作为第一个操作数
usleep(10); // 短暂休眠,增加随机数的离散性
int y = rand() % 5; // 生成0-4的随机数作为第二个操作数
// 从操作符数组中随机选择一个操作符
char op = opers[rand()%opers.size()];
// 创建任务对象
Task t(x, y, op);
// 将任务提交给线程池
tp->Push(t);
// 2. 交给线程池处理
// 输出任务信息,表示主线程创建了什么任务
std::cout << "main thread make task: " << t.GetTask() << std::endl;
// 休眠1秒,控制任务生成速率
sleep(1);
}
}
9.2.2 对线程做一个简单的封装-Thread.hpp
Thread.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <time.h>
#include <pthread.h>
// 定义函数指针类型,用于存储线程回调函数
// 无参数、无返回值的函数指针
typedef void (*callback_t)(); //callback_t 和 void (*)()等价
// 用于生成线程名称的静态计数器
static int num = 1;
class Thread
{
public:
// 静态线程入口函数(pthread_create的要求)
// args: 传入的参数(Thread对象指针)
// 返回值:线程退出时的返回值
static void* Routine(void* args)
{
Thread* thread = static_cast<Thread*>(args); // 将void*转换回Thread*
thread->Entery(); // 调用实际的线程入口函数
return nullptr;
}
public:
// 构造函数
// cb: 线程要执行的回调函数
Thread(callback_t cb) // callback_t cb等价于 void (*cb)()
: tid_(0), // 线程ID初始化为0
name_(""), // 线程名初始化为空
start_timestamp_(0), // 启动时间戳初始化为0
isrunning_(false), // 运行状态初始化为false
cb_(cb) // 设置回调函数
// 表示将构造函数参数 cb 赋值给成员变量 cb_
{}
// 析构函数
~Thread()
{}
// 启动线程
void Run()
{
name_ = "thread-" + std::to_string(num++); // 生成线程名称
start_timestamp_ = time(nullptr); // 记录启动时间戳
isrunning_ = true; // 设置运行状态为true
// 创建线程,传入this指针作为参数
pthread_create(&tid_, nullptr, Routine, this);
/*
int pthread_create(
pthread_t *thread, // 输出参数,存储新创建的线程ID
const pthread_attr_t *attr, // 线程属性,nullptr表示使用默认属性
void *(*start_routine)(void*), // 线程函数
void *arg // 传递给线程函数的参数
);
*/
}
// 等待线程结束
void Join()
{
pthread_join(tid_, nullptr); // 等待线程结束
/*
int pthread_join(pthread_t thread, void **retval);
参数说明:
thread: 要等待的线程的线程ID(pthread_t类型)
retval: 用于存储线程的返回值的指针。如果不关心返回值,可以传入nullptr
返回值:
成功返回0
失败返回错误码
*/
isrunning_ = false; // 设置运行状态为false
}
// 获取线程名称
std::string Name()
{
return name_;
}
// 获取线程启动时间戳
uint64_t StartTimestamp()
{
return start_timestamp_;
}
// 获取线程运行状态
bool IsRunning()
{
return isrunning_;
}
// 实际的线程入口函数
void Entery()
{
cb_(); // 执行回调函数
}
private:
pthread_t tid_; // 线程ID
std::string name_; // 线程名称
uint64_t start_timestamp_; // 启动时间戳
bool isrunning_; // 运行状态标志
callback_t cb_; // 回调函数指针
//等价于 void (*cb)()
};
cpp
#include <iostream>
#include <unistd.h>
#include <vector>
#include "Thread.hpp" // 引入自定义的线程类
using namespace std;
// 测试用的线程函数
// 无限循环打印信息,每秒一次
void Print()
{
while(true)
{
printf("haha, 我是一个封装的线程...\n");
sleep(1); // 休眠1秒
}
}
int main()
{
Thread t(Print); // 创建单个线程对象
t.Run(); // 启动线程
cout << "是否启动成功: " << t.IsRunning() << endl; // 检查线程是否正在运行
cout << "启动成功时间戳: " << t.StartTimestamp() << endl; // 获取线程启动时间戳
cout << "线程的名字: " << t.Name() << endl; // 获取线程名称
t.Join(); // 等待线程结束(已注释)
return 0;
}
9.2.3 单例模式进程池
Task.hpp
cpp
#pragma once
#include <iostream>
#include <string>
// 定义所有支持的运算符集合
std::string opers="+-*/%"; // 加、减、乘、除、取模
// 定义错误码枚举类型
enum {
DivZero=1, // 除零错误
ModZero, // 取模零错误(值为2)
Unknown // 未知错误(值为3)
};
// Task类:表示一个算术运算任务
class Task
{
public:
// 默认构造函数
Task()
{}
// 带参数的构造函数
// x: 第一个操作数
// y: 第二个操作数
// op: 运算符
Task(int x, int y, char op)
: data1_(x), // 初始化第一个操作数
data2_(y), // 初始化第二个操作数
oper_(op), // 初始化运算符
result_(0), // 初始化结果为0
exitcode_(0) // 初始化错误码为0(表示无错误)
{
}
// 执行运算的核心函数
void run()
{
switch (oper_)
{
case '+': // 加法运算
result_ = data1_ + data2_;
break;
case '-': // 减法运算
result_ = data1_ - data2_;
break;
case '*': // 乘法运算
result_ = data1_ * data2_;
break;
case '/': // 除法运算
{
if(data2_ == 0) exitcode_ = DivZero; // 检查除数为0的情况
//如果是0则设置错误代码为DivZero(除零错误)
else result_ = data1_ / data2_;
}
break;
case '%': // 取模运算
{
if(data2_ == 0) exitcode_ = ModZero; // 检查除数为0的情况
else result_ = data1_ % data2_;
}
break;
default: // 未知运算符
exitcode_ = Unknown;
break;
}
}
// 重载函数调用运算符"()",使对象可以像函数一样被调用
void operator ()()
{
run();
}
// 获取完整的运算结果字符串
// 格式:操作数1 运算符 操作数2 = 结果 [错误码]
std::string GetResult()
{
std::string r = std::to_string(data1_); // 转换第一个操作数
r += oper_; // 添加运算符
r += std::to_string(data2_); // 转换第二个操作数
r += "="; // 添加等号
r += std::to_string(result_); // 添加计算结果
r += "[code: "; // 添加错误码标记
r += std::to_string(exitcode_); // 添加错误码
r += "]";
return r;
}
// 获取任务的字符串表示(不包含结果)
// 格式:操作数1 运算符 操作数2 =?
std::string GetTask()
{
std::string r = std::to_string(data1_); // 转换第一个操作数
r += oper_; // 添加运算符
r += std::to_string(data2_); // 转换第二个操作数
r += "=?"; // 添加待计算标记
return r;
}
// 析构函数(目前为空)
~Task()
{
}
private:
int data1_; // 第一个操作数
int data2_; // 第二个操作数
char oper_; // 运算符
int result_; // 计算结果
int exitcode_; // 错误码(0表示无错误)
};
ThreadPool.hpp
cpp
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>
// 线程信息结构体,保存线程ID和名称
struct ThreadInfo {
pthread_t tid; // 线程ID
std::string name; // 线程名称
};
// 默认线程池中线程数量
static const int defalutnum = 5;
template <class T>
class ThreadPool {
public:
// 互斥锁操作封装
void Lock() {
// 对互斥锁加锁,如果锁已被占用则阻塞等待
pthread_mutex_lock(&mutex_);
}
void Unlock() {
// 解锁互斥锁,释放锁的占用
pthread_mutex_unlock(&mutex_);
}
// 条件变量操作封装
// 唤醒一个正在等待该条件变量的线程
void Wakeup() {
// 唤醒一个正在等待该条件变量的线程
// 如果有多个线程等待,则随机唤醒一个
pthread_cond_signal(&cond_);//去任务队列唤醒
}
//线程休眠
void ThreadSleep() {
// 使当前线程在条件变量上等待
// 1. 自动释放互斥锁mutex_
// 2. 线程进入睡眠状态
// 3. 当被唤醒时,自动重新获取互斥锁
pthread_cond_wait(&cond_, &mutex_);
/*
参数和返回值:
参数1 cond:条件变量的指针
参数2 mutex:互斥锁的指针
返回值:成功返回0,失败返回错误码
*/
}
// 判断任务队列是否为空
bool IsQueueEmpty() {
// 返回任务队列的空状态
return tasks_.empty();
}
// 根据线程ID获取线程名称
std::string GetThreadName(pthread_t tid) {
// 遍历线程信息数组
for (const auto &ti : threads_) {
// 找到匹配的线程ID
if (ti.tid == tid)
return ti.name; // 返回对应的线程名
}
return "None"; // 未找到则返回"None"
}
public:
// 1.启动线程池
void Start() {
// 获取线程数量(线程池大小)
int num = threads_.size();
// 循环创建工作线程
for (int i = 0; i < num; i++) {
// 为每个线程设置名称,格式为 "thread-1", "thread-2" 等
// std::to_string 是 C++ 标准库提供的函数,用于将数值类型转换为字符串
threads_[i].name = "thread-" + std::to_string(i + 1);
// 创建线程
pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
/*
int pthread_create(
pthread_t *thread, // 输出参数,存储新创建的线程ID
const pthread_attr_t *attr, // 线程属性,nullptr表示使用默认属性
void *(*start_routine)(void*), // 线程函数
void *arg // 传递给线程函数的参数
);
*/
}
}
// 工作线程的处理函数
static void* HandlerTask(void* args) {
// 将 void* 的参数args转换回线程池指针
ThreadPool<T>* tp = static_cast<ThreadPool<T>*>(args);
//static_cast<>用于进行编译时类型转换。
//语法: static_cast<新类型>(表达式)
//ThreadPool<T>*表示指向ThreadPool<T>类型对象的指针
// 获取当前线程的名称
// pthread_self() 返回当前线程ID
std::string name = tp->GetThreadName(pthread_self());
// 工作线程的主循环
while (true) {
// 先加锁:准备访问共享资源(任务队列)
tp->Lock();
// 当任务队列为空时,进入等待状态
while (tp->IsQueueEmpty()) {
tp->ThreadSleep(); // 条件变量等待
}
// 从任务队列取出一个任务
T t = tp->Pop();
// 取完任务后解锁:不再访问共享资源,让其他线程可以取任务
tp->Unlock();
// 执行任务,它调用了任务对象的 operator() 运算符,也就是实际执行任务的代码
t();//(不需要加锁,因为任务已经安全地取出)
// 输出任务执行结果
std::cout << name << " run, " << "result: " << t.GetResult() << std::endl;
}
}
// 向任务队列添加任务
void Push(const T& t) {
// 加锁保护任务队列
Lock();
// 将任务添加到队列末尾
tasks_.push(t);
Wakeup(); // 唤醒一个等待的工作线程来处理新任务,使用pthread_cond_signal通知
Unlock(); // 解锁,允许其他线程访问任务队列
}
// 从任务队列获取任务
T Pop() {
T t = tasks_.front();
tasks_.pop();
return t;
}
// 获取线程池单例
static ThreadPool<T>* GetInstance() {
// 第一次检查:不加锁
if (nullptr == tp_) { // 双检锁模式的第一次检查
pthread_mutex_lock(&lock_);// 加锁
// 第二次检查:加锁后再次验证
if (nullptr == tp_) { // 双检锁模式的第二次检查
std::cout << "log: singleton create done first!" << std::endl;
tp_ = new ThreadPool<T>();
// 创建一个新的ThreadPool对象并返回指向该对象的指针,将新创建的ThreadPool对象的地址赋值给tp_
}
pthread_mutex_unlock(&lock_);// 解锁
}
return tp_;
/*
只用一次检查且不加锁:多个线程同时进入if语句,会创建多个实例。
直接加锁:每次获取实例都要加锁,即使实例已经创建好了,性能很差。
--------------------------------------------------------------------------
第一次调用GetInstance时:
Thread1 Thread2
检查tp_为空 检查tp_为空
获得锁 等待锁
再次检查tp_为空
创建实例
释放锁 获得锁
检查tp_不为空
直接返回
--------------------------------------------------------------------------
后续调用GetInstance时:
Thread1 Thread2
检查tp_不为空 检查tp_不为空
直接返回 直接返回
--------------------------------------------------------------------------
*/
}
private:
// 构造函数(私有)
// 1. 声明为私有构造函数,禁止外部直接创建对象,保证单例模式
// 2. 参数 num 指定线程池中线程的数量,默认值为 defalutnum(5个线程)
// 3. 使用初始化列表 threads_(num) 创建指定数量的线程信息存储空间
// 4. 初始化互斥锁和条件变量,用于任务队列的线程同步
ThreadPool(int num = defalutnum) : threads_(num) {
pthread_mutex_init(&mutex_, nullptr); // 初始化互斥锁
/*
参数说明:
mutex: 指向 pthread_mutex_t 类型的指针,用于存储要初始化的互斥锁
attr: 指向 pthread_mutexattr_t 类型的指针,用于指定互斥锁的属性
传入 nullptr 表示使用默认属性
默认属性为:快速互斥锁(PTHREAD_MUTEX_FAST_NP)
返回值:
成功返回 0
失败返回错误码
*/
pthread_cond_init(&cond_, nullptr); // 初始化条件变量
/*
参数说明:
cond: 指向 pthread_cond_t 类型的指针,用于存储要初始化的条件变量
attr: 指向 pthread_condattr_t 类型的指针,用于指定条件变量的属性
传入 nullptr 表示使用默认属性
默认属性为:进程私有(PTHREAD_PROCESS_PRIVATE)
返回值:
成功返回 0
失败返回错误码
*/
}
// 析构函数(私有)
// 1. 声明为私有析构函数,禁止外部直接删除对象
// 2. 负责清理线程池资源,销毁互斥锁和条件变量
// 3. 由于使用单例模式,析构函数通常不会被调用(程序结束时系统自动清理)
~ThreadPool() {
pthread_mutex_destroy(&mutex_); // 销毁互斥锁
pthread_cond_destroy(&cond_); // 销毁条件变量
}
// 禁止拷贝构造和赋值
// 1. 使用 delete 关键字删除拷贝构造函数和赋值运算符
// 2. 保证线程池对象不能被复制,维护单例模式的唯一性
// 3. 防止因拷贝导致的资源多重释放问题
ThreadPool(const ThreadPool<T>&) = delete; // 禁用拷贝构造函数
const ThreadPool<T>& operator=(const ThreadPool<T>&) = delete; // 禁用赋值运算符
private:
std::vector<ThreadInfo> threads_; // 工作线程集合
std::queue<T> tasks_; // 任务队列
pthread_mutex_t mutex_; // 任务队列互斥锁
pthread_cond_t cond_; // 任务队列条件变量
static ThreadPool<T>* tp_; // 单例指针
static pthread_mutex_t lock_; // 单例锁
};
// 静态成员初始化
template <class T>
ThreadPool<T>* ThreadPool<T>::tp_ = nullptr;
//nullptr 将单例指针tp_初始化为空指针
template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
//PTHREAD_MUTEX_INITIALIZER 是一个宏,用于初始化互斥锁为默认属性
Main.cpp
cpp
#include <iostream>
#include <ctime>
#include "ThreadPool.hpp" // 线程池头文件
#include "Task.hpp" // 任务类头文件
int main()
{
// 注释提出的问题:如果获取单例对象的时候,也是多线程获取的呢?
// 这个问题涉及到单例模式的线程安全性
// 需要在GetInstance()中使用双检锁或其他线程安全的实现方式
// 启动提示信息
std::cout << "process runn..." << std::endl;
sleep(3); // 程序启动后等待3秒,方便观察启动过程
// 两种创建线程池的方式:
// 1. 直接创建(已注释):
// ThreadPool<Task> *tp = new ThreadPool<Task>(5); // 创建5个线程的线程池
// 2. 使用单例模式(当前使用):
ThreadPool<Task>::GetInstance()->Start(); // 获取线程池实例并启动
// 初始化随机数种子
// 使用当前时间和进程ID异或,增加随机性
srand(time(nullptr) ^ getpid());
// 主循环:持续生成任务
while(true)
{
// 1. 构建任务
int x = rand() % 10 + 1; // 生成1-10的随机数作为第一个操作数
usleep(10); // 短暂休眠,增加随机数的离散性
int y = rand() % 5; // 生成0-4的随机数作为第二个操作数
// 从操作符数组中随机选择一个操作符
char op = opers[rand()%opers.size()];
// 创建任务对象
Task t(x, y, op);
// 将任务提交给线程池
ThreadPool<Task>::GetInstance()->Push(t);
// 2. 交给线程池处理
// 输出任务信息,表示主线程创建了什么任务
std::cout << "main thread make task: " << t.GetTask() << std::endl;
// 休眠1秒,控制任务生成速率
sleep(1);
}
}