首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++ 线程池原理与实现:从零构建一个可复用的线程池

C++ 线程池原理与实现:从零构建一个可复用的线程池

原创
作者头像
用户12719421
发布2026-08-27 15:28:50
发布2026-08-27 15:28:50
1140
举报

C++ 线程池原理与实现:从零构建一个可复用的线程池

原创 · C++ 多线程

多线程程序里,频繁创建和销毁线程的开销并不小:每次创建线程都要分配栈空间、建立内核对象,销毁时还要回收。如果一个任务只需要几毫秒,而线程创建销毁就要花掉其中一大半时间,显然不划算。线程池(Thread Pool)就是为解决这个问题而生的:提前创建好一批工作线程,让它们循环从任务队列里取任务执行,任务提交方只负责"丢任务",完全不关心线程的生死。

本文基于 C++11 标准线程库,从零实现一个可复用的线程池,并对每个关键点做说明。

一、线程池的三个核心组成

一个最简线程池由三部分组成:

  1. 任务队列:存放待执行的任务(std::queue<std::function<void()>>
  2. 一组工作线程:创建后不退出,循环从队列取任务
  3. 同步机制:互斥锁保护队列、条件变量通知"有新任务来了"

所有复杂的线程池,本质都是在这三块上做扩展。

二、完整实现(C++11,可直接编译)

代码语言:cpp
复制
#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() 会阻塞直到结果就绪)。

四、使用示例

代码语言:cpp
复制
#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;
}

输出类似:

代码语言:txt
复制
task in thread 10324
20 + 22 = 42
0 1 4 9 16 25 36 49

五、扩展方向

上面的实现是固定线程数的版本,实际工程里往往还需要:

  • 动态扩容/缩容:任务积压时加线程,空闲时回收,避免线程数长期虚高
  • 任务优先级:把 std::queue 换成 std::priority_queue,按优先级取任务
  • 异常安全task() 外层包 try/catch,避免单个任务抛异常杀死整个工作线程
  • 线程数选择:CPU 密集型取 hardware_concurrency(),I/O 密集型可适当加倍,因为等待 I/O 时不占 CPU

小结

线程池的骨架就三样:任务队列、工作线程、同步原语。本文这个 80 行左右的版本已经覆盖了提交任务、获取结果、优雅关闭等核心能力,可以作为基础继续扩展。理解它的关键,是搞清楚条件变量为什么这样用锁什么时候该放开这两件事。

代码基于 C++11 标准线程库编写,在主流编译器(GCC/Clang/MSVC)下可直接编译运行。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • C++ 线程池原理与实现:从零构建一个可复用的线程池
    • 一、线程池的三个核心组成
    • 二、完整实现(C++11,可直接编译)
    • 三、关键点解析
    • 四、使用示例
    • 五、扩展方向
    • 小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档