实战:线程池(C++11线程+任务队列)

线程池这东西,说白了就是「提前招好一批工人,有活来了直接派活,不用现招现辞」。我在做后台服务的时候,几乎每个高并发项目都离不开它。你想想看,如果每个请求来了都 new 一个线程,处理完再 delete,光是线程创建销毁的开销就能把性能吃光。

今天咱们就手写一个轻量级的线程池。用 C++11 的 std::thread 和 std::queue 来实现。嗯,代码量不大,但设计思路很经典。

为什么需要线程池?

我刚开始写多线程程序时,也踩过这个坑。每次来任务就开线程,结果发现:

  • 线程创建销毁太慢,比任务本身还耗时
  • 线程数量失控,系统资源被耗尽
  • 上下文切换频繁,CPU 都在做无用功

线程池的核心思想就三个字:复用。固定数量的线程,循环从任务队列里取任务执行。任务来了就排队,线程空闲了就取走执行。

核心要点:线程池 = 固定线程数 + 线程安全的任务队列 + 同步机制

整体架构设计

先画个图,让你一眼看明白线程池的内部结构:

线程池架构图 任务队列 std::queue<Task> push / pop 互斥锁保护 取出 线程池 线程 1 (工作) 线程 2 (空闲) 线程 3 (等待) 执行 执行结果 任务完成 回调通知 控制信号 停止标志 / 条件变量

代码实现

好,直接上代码。我习惯把线程池封装成一个类,接口尽量简洁:

#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