C++ 线程池(ThreadPool)完整详解
本文详细讲解一个经典、实用的 C++11 线程池实现。内容涵盖:
- 设计目标
- 核心数据结构
- 线程生命周期
- 任务提交与执行流程
- 同步机制原理
- RAII 与异常安全
- 优缺点与可改进方向
一、线程池是什么?
线程池(Thread Pool) 是一种并发编程技术,用来:
- 预先创建一组线程
- 统一管理这些线程
- 反复执行大量短生命周期任务
为什么要使用线程池?
如果每来一个任务就:
std::thread t(f);
t.join();
会带来以下问题:
- 线程创建/销毁开销大
- 频繁系统调用
- 无法控制并发线程数量
- 性能不可控
线程池的核心思想:
用有限的线程,处理无限的任务
二、整体设计思路
该线程池采用 生产者-消费者模型:
- 生产者:调用
enqueue()的用户线程 - 消费者:线程池中的 worker 线程
- 缓冲区:任务队列
std::queue<std::function<void()>>
示意图:
主线程 ── enqueue() ──► 任务队列 ──► 工作线程
▲ │
└── wait/notify
三、头文件与核心成员
class ThreadPool {
public:
explicit ThreadPool(size_t threads);
~ThreadPool();
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>;
private:
std::vector<std::thread> workers; // 工作线程
std::queue<std::function<void()>> tasks; // 任务队列
std::mutex queue_mutex; // 互斥锁
std::condition_variable condition; // 条件变量
bool stop; // 停止标志
};
成员职责说明
| 成员 | 作用 |
|---|---|
workers | 保存所有工作线程 |
tasks | 保存待执行任务 |
queue_mutex | 保护任务队列 |
condition | 线程等待 / 唤醒 |
stop | 控制线程池生命周期 |
四、构造函数:启动线程池
ThreadPool::ThreadPool(size_t threads) : stop(false)
核心逻辑
- 创建
threads个 worker - 每个 worker 执行一个 无限循环
for (;;) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mutex);
condition.wait(lock, [this] {
return stop || !tasks.empty();
});
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
关键点拆解
1️⃣ 条件变量 wait
condition.wait(lock, predicate);
等价于:
- 自动释放锁
- 进入阻塞
- 被唤醒后重新加锁
- 判断条件是否成立
2️⃣ 正确退出线程
if (stop && tasks.empty()) return;
确保:
- 析构时线程能退出
- 队列中已有任务仍能执行完
3️⃣ 锁外执行任务
task();
避免:
- 阻塞其他线程取任务
- 串行化执行
五、enqueue:提交任务
函数模板签名
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>;
📌 特点:
- 支持任意函数 / lambda
- 支持任意返回值
- 支持参数完美转发
六、任务包装机制(重点)
1️⃣ 推导返回类型
using return_type = typename std::result_of<F(Args...)>::type;
2️⃣ packaged_task + future
auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
关系图:
packaged_task ──► future
│
└── function()
为什么要用 shared_ptr?
packaged_task不可拷贝std::function要求可拷贝- 用
shared_ptr间接解决
七、任务入队流程
{
std::unique_lock<std::mutex> lock(queue_mutex);
if (stop) throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace([task](){ (*task)(); });
}
condition.notify_one();
并发安全保证
| 操作 | 是否加锁 |
|---|---|
| 检查 stop | ✅ |
| 插入任务 | ✅ |
| 执行任务 | ❌ |
八、析构函数:安全关闭线程池
ThreadPool::~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
condition.notify_all();
for (auto &worker : workers) worker.join();
}
析构流程
- 设置
stop = true - 唤醒所有线程
- 等待线程退出
📌 RAII 思想:
对象生命周期 = 资源生命周期
九、线程池的优点
✅ 简洁清晰
✅ 完全 C++11 标准
✅ 异常安全
✅ 支持返回值
✅ 广泛工程可用
十、完整的代码
#ifndef THREAD_POOL_H
#define THREAD_POOL_H
#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>
class ThreadPool {
public:
// 构造函数:初始化并启动指定数量的工作线程
explicit ThreadPool(size_t threads);
// 析构函数:停止所有线程并清理资源
~ThreadPool();
// 核心方法:向线程池提交任务
// F: 任务函数类型, Args: 参数包
// 返回值: std::future,用于异步获取任务结果
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>;
private:
// 工作线程数组
std::vector< std::thread > workers;
// 任务队列:存储类型擦除后的 void() 函数对象
std::queue< std::function<void()> > tasks;
// 同步原语
std::mutex queue_mutex; // 互斥锁:保护任务队列
std::condition_variable condition; // 条件变量:用于线程间的 等待/唤醒
bool stop; // 停止标志:控制线程池生命周期
};
// 构造函数实现
inline ThreadPool::ThreadPool(size_t threads)
: stop(false)
{
// 循环创建工作线程
for(size_t i = 0; i < threads; ++i)
workers.emplace_back(
[this]
{
// 每个线程内部是一个死循环,不断尝试获取任务
for(;;)
{
std::function<void()> task;
{
// 1. 获取锁,准备访问任务队列
std::unique_lock<std::mutex> lock(this->queue_mutex);
// 2. 等待条件满足:
// wait 会自动释放锁并阻塞线程,直到被 notify 唤醒。
// 唤醒后会检查 lambda 谓词:
// 如果 (stop == true) 或者 (任务队列不为空),则停止等待,继续向下执行。
// 否则继续阻塞等待。
this->condition.wait(lock,
[this]{ return this->stop || !this->tasks.empty(); });
// 3. 退出判断:
// 如果线程池叫停 且 任务队列已空,则该线程结束运行
if(this->stop && this->tasks.empty())
return;
// 4. 取出任务 (使用 std::move 避免拷贝代价)
task = std::move(this->tasks.front());
this->tasks.pop();
}
// 锁在这里自动释放 (lock 离开作用域)
// 5. 执行任务 (在锁外执行,保证并发性能)
task();
}
}
);
}
// 任务提交函数实现
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
// 推导任务返回类型
using return_type = typename std::result_of<F(Args...)>::type;
// 1. 包装任务:
// std::bind: 将函数和参数绑定为一个可调用对象
// std::packaged_task: 包装可调用对象,使其结果可以通过 future 获取
// std::make_shared: 使用智能指针管理任务,因为 packaged_task 不可拷贝,只能移动,
// 而 std::function 需要可拷贝对象,所以用 shared_ptr 包装一层。
auto task = std::make_shared< std::packaged_task<return_type()> >(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
// 2. 获取 future,以便返回给调用者
std::future<return_type> res = task->get_future();
{
// 3. 加锁访问任务队列 (临界区)
std::unique_lock<std::mutex> lock(queue_mutex);
// 如果线程池已停止,禁止提交新任务
if(stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
// 4. 将任务加入队列
// 这里的 lambda 只是为了适配 queue<function<void()>> 的类型
// 实际执行时是调用智能指针指向的 packaged_task
tasks.emplace([task](){ (*task)(); });
}
// 5. 唤醒一个正在 wait 的工作线程来处理这个任务
condition.notify_one();
return res;
}
// 析构函数实现
inline ThreadPool::~ThreadPool()
{
{
// 1. 加锁并设置停止标志
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
// 2. 唤醒所有线程
// 它们醒来后会检查 stop 标志。如果队列空了,它们就会 return 退出循环。
condition.notify_all();
// 3. 等待所有线程执行完毕 (join)
// 确保主线程不会在子线程结束前退出
for(std::thread &worker: workers)
worker.join();
}
#endif
十一、可改进方向(进阶)
| 改进项 | 说明 |
|---|---|
使用 std::invoke | 替代 std::bind |
使用 std::invoke_result_t | 替代 result_of(C++17) |
| 任务优先级 | priority_queue |
| 动态线程数量 | auto scaling |
| 工作窃取 | 提升负载均衡 |
| 停止策略 | shutdown_now() |