Skip to content

线程池设计(HPC 必考手写)

一、为什么需要线程池

  • 线程创建/销毁开销大(系统调用、栈 8MB 虚映射、调度器状态)。
  • 高频任务若每任务开线程 → 调度器压力 + 上下文切换爆炸 + 缓存冷。
  • 线程池:固定 N 个常驻工作线程 + 共享任务队列,主线程只投递任务,吞吐和延迟稳定。

HPC 用途:并行 for、流水线 stage(解压→预处理→计算→写出)、推理批处理 worker、异步 IO 回调。

二、核心设计要素

  1. 工作线程数 W:CPU 密集 ≈ hardware_concurrency()(扣掉默认归一的 AVX512 频率考量);混合 IO 按需放大。
  2. 任务队列:经典单锁 + std::condition_variable(互斥队列);高性能用无锁 MPMC 队列每线程本地队列 + 工作窃取
  3. 任务模型std::function<void()>(需类型擦除,小任务可能堆分配),或侵入式任务节点(链表、避免每任务 alloc)。
  4. 优雅关闭:stop 标志 + notify_all + join。
  5. 扩展:返回值(future)、优先级、批量提交、依赖(DAG)。

三、经典互斥版线程池(面试能写对即过)

cpp
#include <condition_variable>
#include <functional>
#include <future>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>

class ThreadPool {
public:
    explicit ThreadPool(size_t n)
        : stop_(false) {
        for (size_t i = 0; i < n; ++i)
            workers_.emplace_back([this]{ workerLoop(); });
    }

    ~ThreadPool() {
        { std::lock_guard<std::mutex> lk(m_); stop_ = true; }
        cv_.notify_all();
        for (std::thread& t : workers_) if (t.joinable()) t.join();
    }

    // 提交并返回 future;完美转发,形参列表给 emplace
    template <class F, class... Args>
    auto submit(F&& f, Args&&... args)
        -> std::future<std::invoke_result_t<F, Args...>> {
        using R = std::invoke_result_t<F, Args...>;
        auto task = std::make_shared<std::packaged_task<R()>>(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...));
        std::future<R> res = task->get_future();
        {
            std::lock_guard<std::mutex> lk(m_);
            if (stop_) throw std::runtime_error("thread pool stopped");
            tasks_.emplace([task]{ (*task)(); });
        }
        cv_.notify_one();
        return res;
    }

private:
    void workerLoop() {
        for (;;) {
            std::function<void()> job;
            {
                std::unique_lock<std::mutex> lk(m_);
                cv_.wait(lk, [&]{ return stop_ || !tasks_.empty(); });
                if (stop_ && tasks_.empty()) return;
                job = std::move(tasks_.front());
                tasks_.pop();
            }
            job();
        }
    }

    std::vector<std::thread> workers_;
    std::queue<std::function<void()>> tasks_;
    std::mutex m_;
    std::condition_variable cv_;
    bool stop_;
};

面试追问点应对

  • 为什么 packaged_task 包一层 shared_ptr + lambda?—— packaged_task 不可拷贝,放队列需可移动;shared 让 lambda 可拷贝捕获,返回 future 外传。
  • 为什么 submit 里 lock 后还判 stop_?—— 池已关闭再投递应报错而非静默丢任务。
  • 析构顺序 / join:先置 stop、notify_all(唤醒全部沉睡线程),再逐个 join(见注释);若线程仍在跑大任务会阻塞直到完成——生产可加「等待当前任务完成再退出」语义。
  • 竞态:worker 判 stop 且队列空才退出 —— 防止「停了但队列还有残留任务没跑完」的丢任务。

四、进阶:无锁任务队列 & 工作窃取(work-stealing)

单锁队列在多生产者多消费者下争用高。高性能池方案:

  1. 每线程一个本地任务 deque(LIFO 取自己刚投的,缓存友好)
  2. 本地空时去其它线程队尾 steal(MPMC 部分)
  3. 本地结构做成无锁(CAS 管理头尾),降低往返。

依赖实现库里通常叫 work-stealing scheduler(如 TBB task_group、OneTBB)。框架题常考:任务递归拆分→本地压栈→末端 steal。

cpp
// 每工作线程:
struct ThreadCtx {
    std::deque<Task> local;   // 简化;真正无锁用 ring/stack + CAS
    bool isBusy();
    Task steal();             // 从其它 worker 队尾取
};

能答出「本地 LIFO + 远端 steal FIFO → 更好 cache 与负载均衡」即显著加分;再提 TBB/enkiTS/Intel 实现即可。

五、HPC 追问:什么时候别用线程池

  • 任务粒度 < 调度开销(几微秒级)→ 直接串行或 SIMD/向量化。
  • 有明确依赖/数据分块 → 用并行算法(tbb::parallel_for)而非手写循环。
  • GPU 卸载这类工作 → 交给 GPU/设备队列,CPU 池不用管。

六、易错清单(八股高频)

  1. cv.wait 必须配 unique_lock 且带谓词
  2. notify 别持锁(否则唤醒了又阻塞在拿锁)。
  3. 析构时先 stop + notify_all,再 join——顺序不能反。
  4. 任务队列存 movable,别存不可移动的 packaged_task 裸体。
  5. 异常:任务内异常会被 future.get() 拿到;run-to-completion 池要把异常捕获,别让 worker 崩。

C++ 面试八股 · VitePress 版