目录
[1. 线程池的基本结构](#1. 线程池的基本结构)
[2. 代码实现](#2. 代码实现)
[3. 任务类的设计](#3. 任务类的设计)
[4. 主线程逻辑](#4. 主线程逻辑)
一、线程池的概念
线程池是一种线程使用模式,它通过维护一组预先创建的线程来高效地处理任务。线程池的核心思想是避免频繁地创建和销毁线程,因为线程的创建和销毁会带来系统调度开销,并可能影响缓存局部性和整体性能。线程池中的线程处于等待状态,当有任务需要处理时,线程池会将任务分配给空闲的线程执行。

🍌上面这张图片展示了一个典型的线程池(ThreadPool)工作原理:
- 调用线程(调用线程-1、调用线程-2、...、调用线程-N)
-
这些是客户端或应用程序中的线程,它们负责将任务提交到线程池中。
-
每个调用线程将任务(例如,一个可执行的代码块或任务对象)提交到线程池的队列中。
- 线程池
-
线程池是一个容器,用于管理一组预创建的线程(称为"处理线程"),这些线程可以重复使用来执行任务。
-
线程池的主要目的是减少频繁创建和销毁线程的开销,提高系统性能。
- 队列
-
队列是线程池中的一个核心组件,用于存储提交的任务。
-
当调用线程提交任务时,任务会被放入队列中等待处理。
-
队列通常是先进先出(FIFO)的,但具体实现可能根据线程池的配置有所不同。
- 处理线程(处理线程-1、处理线程-2、...、处理线程-M)
-
这些是线程池中预先创建的线程,负责从队列中取出任务并执行。
-
每个处理线程会不断检查队列中是否有任务,如果有,则取出任务并执行;如果没有,则进入等待状态。
-
处理线程的数量(M)通常是固定的,由线程池的配置决定。
-
工作流程
-
任务提交:调用线程将任务提交到线程池的队列中。
-
任务存储:任务被存储在队列中,等待处理。
-
任务分配:处理线程从队列中取出任务并执行。
-
任务完成:处理线程完成任务后,继续从队列中获取新的任务。
二、线程池的优点
-
避免短任务的线程开销:对于短时间任务,线程池可以复用线程,避免频繁创建和销毁线程的代价。
-
充分利用系统资源:线程池可以根据系统资源(如处理器核心数、内存等)合理分配线程数量,避免资源浪费。
-
防止过度调度:线程池通过限制线程数量,避免因线程过多导致的调度开销。
三、线程池的应用场景
🌲线程池适用于以下场景:
-
大量短任务:例如Web服务器处理网页请求,任务数量巨大但单个任务处理时间短。
-
高性能要求:例如服务器需要快速响应客户端请求。
-
突发性大量请求:例如短时间内大量客户端请求,线程池可以避免因创建大量线程导致的系统崩溃。
四、线程池的实现
1. 线程池的基本结构
🍑线程池的核心组件包括:
-
任务队列:存储待处理的任务。
-
线程池:维护一组线程,负责从任务队列中获取任务并执行。
-
互斥锁和条件变量:用于保护任务队列,确保线程安全。
2. 代码实现
cpp
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#define NUM 5 // 默认线程池中线程的数量
// 线程池类模板
template<class T>
class ThreadPool
{
private:
// 判断任务队列是否为空
bool IsEmpty()
{
return _task_queue.size() == 0;
}
// 锁定任务队列
void LockQueue()
{
pthread_mutex_lock(&_mutex);
}
// 解锁任务队列
void UnLockQueue()
{
pthread_mutex_unlock(&_mutex);
}
// 等待条件变量
void Wait()
{
pthread_cond_wait(&_cond, &_mutex);
}
// 唤醒条件变量
void WakeUp()
{
pthread_cond_signal(&_cond);
}
public:
// 构造函数
ThreadPool(int num = NUM)
: _thread_num(num)
{
// 初始化互斥锁和条件变量
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
// 析构函数
~ThreadPool()
{
// 销毁互斥锁和条件变量
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
// 线程池中线程的执行例程(静态方法)
static void* Routine(void* arg)
{
pthread_detach(pthread_self()); // 将线程设置为分离状态
ThreadPool* self = (ThreadPool*)arg; // 获取线程池对象
// 线程不断从任务队列中获取任务并执行
while (true)
{
self->LockQueue(); // 锁定任务队列
while (self->IsEmpty()) // 如果任务队列为空,等待
{
self->Wait();
}
T task;
self->Pop(task); // 从任务队列中取出任务
self->UnLockQueue(); // 解锁任务队列
task.Run(); // 执行任务
}
return nullptr;
}
// 初始化线程池中的线程
void ThreadPoolInit()
{
pthread_t tid;
for (int i = 0; i < _thread_num; i++)
{
// 创建线程并传入线程池对象的this指针
pthread_create(&tid, nullptr, Routine, this);
}
}
// 向任务队列中添加任务
void Push(const T& task)
{
LockQueue(); // 锁定任务队列
_task_queue.push(task); // 将任务加入队列
UnLockQueue(); // 解锁任务队列
WakeUp(); // 唤醒一个等待的线程
}
// 从任务队列中取出任务
void Pop(T& task)
{
task = _task_queue.front(); // 获取队列头部任务
_task_queue.pop(); // 移除队列头部任务
}
private:
std::queue<T> _task_queue; // 任务队列
int _thread_num; // 线程池中线程的数量
pthread_mutex_t _mutex; // 互斥锁
pthread_cond_t _cond; // 条件变量
};
3. 任务类的设计
任务类需要包含一个Run
方法,用于执行任务的逻辑:
cpp
#pragma once
#include <iostream>
// 任务类
class Task
{
public:
// 构造函数
Task(int x = 0, int y = 0, char op = 0)
: _x(x), _y(y), _op(op)
{}
// 析构函数
~Task()
{}
// 执行任务的方法
void Run()
{
int result = 0;
switch (_op)
{
case '+':
result = _x + _y;
break;
case '-':
result = _x - _y;
break;
case '*':
result = _x * _y;
break;
case '/':
if (_y == 0)
{
std::cerr << "Error: division by zero!" << std::endl;
return;
}
else
{
result = _x / _y;
}
break;
case '%':
if (_y == 0)
{
std::cerr << "Error: modulo by zero!" << std::endl;
return;
}
else
{
result = _x % _y;
}
break;
default:
std::cerr << "Error: invalid operation!" << std::endl;
return;
}
std::cout << "Thread [" << pthread_self() << "]: " << _x << " " << _op << " " << _y << " = " << result << std::endl;
}
private:
int _x; // 操作数1
int _y; // 操作数2
char _op; // 操作符
};
4. 主线程逻辑
主线程负责向任务队列中添加任务,线程池中的线程会自动处理这些任务:
cpp
#include "Task.hpp"
#include "ThreadPool.hpp"
int main()
{
srand((unsigned int)time(nullptr)); // 初始化随机数种子
// 创建线程池并初始化
ThreadPool<Task>* tp = new ThreadPool<Task>;
tp->ThreadPoolInit();
const char* op = "+-*/%"; // 操作符列表
// 不断向任务队列中添加任务
while (true)
{
sleep(1); // 每秒添加一个任务
int x = rand() % 100; // 随机生成操作数1
int y = rand() % 100; // 随机生成操作数2
int index = rand() % 5; // 随机选择操作符
Task task(x, y, op[index]); // 创建任务
tp->Push(task); // 将任务加入线程池
}
return 0;
}

五、常见问题
- 为什么在线程池的
Routine
函数中使用while
循环检查任务队列是否为空?
原因 :条件变量可能存在伪唤醒 (即线程被唤醒不是因为条件满足,而是由于系统信号或其他原因)。使用
while
循环可以确保在被唤醒后再次检查条件,避免任务队列实际为空时错误地执行任务。代码示例:
cppwhile (self->IsEmpty()) { self->Wait(); }
对比
if
:如果使用if
,伪唤醒可能导致线程试图从空队列中取任务,引发未定义行为(如崩溃)。
- 为什么
Routine
函数需要是静态方法?如何访问类的成员?
C++限制 :
pthread_create
要求线程函数是static
,因为它没有隐式的this
指针。访问成员 :通过将线程池对象的指针(
this
)作为参数传递给Routine
,可以在静态方法中访问非静态成员:
cppstatic void* Routine(void* arg) { ThreadPool* self = (ThreadPool*)arg; self->LockQueue(); // 访问成员函数 }
- 互斥锁和条件变量的作用是什么?
互斥锁(
pthread_mutex_t
) :保护任务队列(_task_queue
),确保同一时间只有一个线程访问队列,防止数据竞争。条件变量(
pthread_cond_t
) :协调线程的等待与唤醒。当队列为空时,线程通过pthread_cond_wait
挂起;当新任务加入时,通过pthread_cond_signal
唤醒一个线程。
- 如何扩展线程池以处理不同类型的任务?
模板设计 :当前线程池使用模板类
ThreadPool<T>
,只需为不同任务类型实现对应的Run
方法即可。示例:若需处理网络请求,可以定义新的任务类:
cppclass NetworkTask { public: void Run() { // 处理网络请求的逻辑 } }; ThreadPool<NetworkTask> network_pool;
- 条件变量为何使用
signal
而非broadcast
?
- 避免惊群效应 :
pthread_cond_signal
唤醒一个线程,而pthread_cond_broadcast
唤醒所有等待线程。使用signal
减少不必要的竞争,尤其在任务队列中每次只添加一个任务时更高效。
- 主线程中的
sleep(1)
是否合理?
- 模拟场景 :示例中
sleep(1)
用于降低任务生成速度,便于观察输出。实际应用中,应根据需求调整任务生产速率(如事件驱动或实时接收请求)。