实战:线程池(C++11线程+任务队列)
线程池这东西,说白了就是「提前招好一批工人,有活来了直接派活,不用现招现辞」。我在做后台服务的时候,几乎每个高并发项目都离不开它。你想想看,如果每个请求来了都 new 一个线程,处理完再 delete,光是线程创建销毁的开销就能把性能吃光。
今天咱们就手写一个轻量级的线程池。用 C++11 的 std::thread 和 std::queue 来实现。嗯,代码量不大,但设计思路很经典。
为什么需要线程池?
我刚开始写多线程程序时,也踩过这个坑。每次来任务就开线程,结果发现:
- 线程创建销毁太慢,比任务本身还耗时
- 线程数量失控,系统资源被耗尽
- 上下文切换频繁,CPU 都在做无用功
线程池的核心思想就三个字:复用。固定数量的线程,循环从任务队列里取任务执行。任务来了就排队,线程空闲了就取走执行。
核心要点:线程池 = 固定线程数 + 线程安全的任务队列 + 同步机制
整体架构设计
先画个图,让你一眼看明白线程池的内部结构:
代码实现
好,直接上代码。我习惯把线程池封装成一个类,接口尽量简洁:
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
#include <memory>
class ThreadPool {
public:
// 构造函数:启动 num_threads 个工作线程
explicit ThreadPool(size_t num_threads)
: stop_(false) {
for (size_t i = 0; i < num_threads; ++i) {
workers_.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->queue_mutex_);
this->condition_.wait(lock, [this] {
return this->stop_ || !this->tasks_.empty();
});
if (this->stop_ && this->tasks_.empty())
return;
task = std::move(this->tasks_.front());
this->tasks_.pop();
}
task();
}
});
}
}
// 提交任务,返回 std::future 获取结果
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type> {
using return_type = typename std::result_of<F(Args...)>::type;
auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> res = task->get_future();
{
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();
return res;
}
// 析构函数:停止所有线程
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex_);
stop_ = true;
}
condition_.notify_all();
for (std::thread &worker : workers_)
worker.join();
}
private:
std::vector<std::thread> workers_; // 工作线程
std::queue<std::function<void()>> tasks_; // 任务队列
std::mutex queue_mutex_; // 互斥锁
std::condition_variable condition_; // 条件变量
bool stop_; // 停止标志
};
关键设计点解析
这段代码看着不长,但每个细节都有讲究。我一个个说:
1. 线程的等待与唤醒
每个线程启动后,会进入一个无限循环。关键就在 condition_.wait() 这里。线程会一直等,直到满足两个条件之一:有任务可做,或者线程池要关闭了。这样避免了忙等待(busy waiting),CPU 占用率几乎为零。
我的习惯:条件变量的 wait 一定要配合 lambda 判断条件,防止虚假唤醒(spurious wakeup)。这是 C++ 标准允许的,不检查的话可能会出 bug。
2. 任务队列的线程安全
所有对 tasks_ 的访问都通过 queue_mutex_ 保护。入队和出队操作都加锁,保证多线程环境下不会出现数据竞争。
3. 返回值的处理
用 std::packaged_task 包装任务,配合 std::future 获取返回值。这样调用方可以异步等待结果,非常灵活。
使用示例
来看看怎么用这个线程池:
#include <iostream>
int main() {
// 创建 4 个线程的线程池
ThreadPool pool(4);
// 提交多个任务
auto result1 = pool.enqueue([](int a, int b) {
return a + b;
}, 10, 20);
auto result2 = pool.enqueue([](int x) {
return x * x;
}, 7);
// 获取结果(会阻塞直到任务完成)
std::cout << "10 + 20 = " << result1.get() << std::endl;
std::cout << "7 * 7 = " << result2.get() << std::endl;
// 批量提交
std::vector<std::future<int>> results;
for (int i = 0; i < 10; ++i) {
results.emplace_back(
pool.enqueue([i] {
return i * 2;
})
);
}
for (auto &res : results) {
std::cout << res.get() << " ";
}
std::cout << std::endl;
return 0;
}
避坑指南
我曾经踩过的坑:
- 线程数设置:不是越多越好。我一般设为
std::thread::hardware_concurrency(),也就是 CPU 核心数。IO 密集型任务可以适当增加。 - 任务抛异常:如果任务内部抛异常没捕获,整个线程会挂掉。建议在任务函数内部 try-catch,或者用
std::packaged_task自动捕获异常。 - 死锁风险:不要在任务中等待另一个提交到同一个线程池的任务。这会导致线程饥饿(thread starvation)。
性能对比
我拿一个实际项目的数据给你看看:处理 10000 个计算密集型任务,每个任务耗时约 10ms。
| 方案 | 总耗时 | CPU 占用 | 内存波动 |
|---|---|---|---|
| 每任务新建线程 | 约 120 秒 | 高(频繁创建销毁) | 大 |
| 线程池(4线程) | 约 25 秒 | 平稳 | 小 |
| 线程池(8线程) | 约 13 秒 | 平稳 | 小 |
看到了吧?线程池的优势非常明显。而且线程数不是越多越好,8 线程比 4 线程快一倍,但 16 线程可能反而因为上下文切换变慢。
扩展思考
这个基础版本已经能应对大部分场景了。如果你想要更高级的功能,可以考虑:
- 动态调整线程数:根据任务队列长度自动增减线程
- 优先级队列:用
std::priority_queue替代普通队列 - 任务超时机制:给任务设置最大执行时间
- 工作窃取(work stealing):每个线程有自己的任务队列,空闲时偷别人的任务
嗯,这些就留给有兴趣的同学自己去探索了。线程池这个模式,说白了就是「空间换时间」——用固定数量的线程资源,换取任务调度的效率。理解了这一点,你就能灵活运用了。
公众号:蓝海资料掘金营,微信deep3321