原创 · C++ 多线程
多线程程序里,频繁创建和销毁线程的开销并不小:每次创建线程都要分配栈空间、建立内核对象,销毁时还要回收。如果一个任务只需要几毫秒,而线程创建销毁就要花掉其中一大半时间,显然不划算。线程池(Thread Pool)就是为解决这个问题而生的:提前创建好一批工作线程,让它们循环从任务队列里取任务执行,任务提交方只负责"丢任务",完全不关心线程的生死。
本文基于 C++11 标准线程库,从零实现一个可复用的线程池,并对每个关键点做说明。
一个最简线程池由三部分组成:
std::queue<std::function<void()>>)所有复杂的线程池,本质都是在这三块上做扩展。
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
#include <memory>
#include <stdexcept>
class ThreadPool {
public:
// 默认线程数 = 硬件并发数(CPU 核心数)
explicit ThreadPool(size_t threads = std::thread::hardware_concurrency())
: stop_(false) {
for (size_t i = 0; i < threads; ++i) {
workers_.emplace_back([this] {
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();
}
});
}
}
// 提交任务,返回 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 (auto& 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() 处休眠,enqueue 提交任务后调用 notify_one() 唤醒一个线程。相比"轮询 + sleep"的空转,条件变量在没有任务时让线程真正挂起,几乎不占 CPU。wait 的第二个参数是谓词,用来防止虚假唤醒(spurious wakeup)——被唤醒后发现既没有任务也没有停止,就继续等。
2. 为什么在锁外执行任务?
代码里 task() 是在释放锁之后调用的。如果带着锁执行任务,一旦某个任务执行时间很长,其他线程想取任务、主线程想 enqueue 都会被卡住。把执行移出临界区是线程池性能的关键细节。
3. 优雅关闭为什么是"停止 + 排空"两步?
析构时先把 stop_ 置为 true,再 notify_all()。每个工作线程被唤醒后检查 stop_ && tasks_.empty()——只有停止且队列已空才退出,这样已经入队的任务不会被丢。如果不等队列排空直接退出,调用方会丢失任务,这是新手最常见的坑。
4. 任务结果怎么拿回来?
enqueue 返回 std::future<return_type>。内部用 std::packaged_task 把"可调用对象 + 返回值"绑定在一起,任务执行完,调用方通过 future.get() 同步拿到结果(get() 会阻塞直到结果就绪)。
#include <iostream>
#include <chrono>
int main() {
ThreadPool pool(4);
// 提交无返回值的任务
pool.enqueue([] {
std::cout << "task in thread " << std::this_thread::get_id() << "\n";
});
// 提交带参数、带返回值的任务
auto fut = pool.enqueue([](int a, int b) { return a + b; }, 20, 22);
std::cout << "20 + 22 = " << fut.get() << "\n";
// 批量提交
std::vector<std::future<int>> results;
for (int i = 0; i < 8; ++i)
results.emplace_back(pool.enqueue([i] { return i * i; }));
for (auto& r : results)
std::cout << r.get() << " ";
std::cout << "\n";
return 0;
}输出类似:
task in thread 10324
20 + 22 = 42
0 1 4 9 16 25 36 49上面的实现是固定线程数的版本,实际工程里往往还需要:
std::queue 换成 std::priority_queue,按优先级取任务task() 外层包 try/catch,避免单个任务抛异常杀死整个工作线程hardware_concurrency(),I/O 密集型可适当加倍,因为等待 I/O 时不占 CPU线程池的骨架就三样:任务队列、工作线程、同步原语。本文这个 80 行左右的版本已经覆盖了提交任务、获取结果、优雅关闭等核心能力,可以作为基础继续扩展。理解它的关键,是搞清楚条件变量为什么这样用和锁什么时候该放开这两件事。
代码基于 C++11 标准线程库编写,在主流编译器(GCC/Clang/MSVC)下可直接编译运行。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。