C++ 11 线程池

65 阅读6分钟

C++ 线程池(ThreadPool)完整详解

本文详细讲解一个经典、实用的 C++11 线程池实现。内容涵盖:

  • 设计目标
  • 核心数据结构
  • 线程生命周期
  • 任务提交与执行流程
  • 同步机制原理
  • RAII 与异常安全
  • 优缺点与可改进方向

一、线程池是什么?

线程池(Thread Pool) 是一种并发编程技术,用来:

  • 预先创建一组线程
  • 统一管理这些线程
  • 反复执行大量短生命周期任务

为什么要使用线程池?

如果每来一个任务就:

std::thread t(f);
t.join();

会带来以下问题:

  • 线程创建/销毁开销大
  • 频繁系统调用
  • 无法控制并发线程数量
  • 性能不可控

线程池的核心思想:

用有限的线程,处理无限的任务


二、整体设计思路

该线程池采用 生产者-消费者模型

  • 生产者:调用 enqueue() 的用户线程
  • 消费者:线程池中的 worker 线程
  • 缓冲区:任务队列 std::queue<std::function<void()>>

示意图:

主线程 ── enqueue() ──► 任务队列 ──► 工作线程
                               ▲          │
                               └── wait/notify

三、头文件与核心成员

class ThreadPool {
public:
    explicit ThreadPool(size_t threads);
    ~ThreadPool();

    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args)
        -> std::future<typename std::result_of<F(Args...)>::type>;

private:
    std::vector<std::thread> workers;      // 工作线程
    std::queue<std::function<void()>> tasks; // 任务队列

    std::mutex queue_mutex;                // 互斥锁
    std::condition_variable condition;     // 条件变量
    bool stop;                             // 停止标志
};

成员职责说明

成员作用
workers保存所有工作线程
tasks保存待执行任务
queue_mutex保护任务队列
condition线程等待 / 唤醒
stop控制线程池生命周期

四、构造函数:启动线程池

ThreadPool::ThreadPool(size_t threads) : stop(false)

核心逻辑

  • 创建 threads 个 worker
  • 每个 worker 执行一个 无限循环
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();
}

关键点拆解

1️⃣ 条件变量 wait
condition.wait(lock, predicate);

等价于:

  • 自动释放锁
  • 进入阻塞
  • 被唤醒后重新加锁
  • 判断条件是否成立
2️⃣ 正确退出线程
if (stop && tasks.empty()) return;

确保:

  • 析构时线程能退出
  • 队列中已有任务仍能执行完
3️⃣ 锁外执行任务
task();

避免:

  • 阻塞其他线程取任务
  • 串行化执行

五、enqueue:提交任务

函数模板签名

template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
    -> std::future<typename std::result_of<F(Args...)>::type>;

📌 特点

  • 支持任意函数 / lambda
  • 支持任意返回值
  • 支持参数完美转发

六、任务包装机制(重点)

1️⃣ 推导返回类型

using return_type = typename std::result_of<F(Args...)>::type;

2️⃣ packaged_task + future

auto task = std::make_shared<std::packaged_task<return_type()>>(
    std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);

关系图:

packaged_task ──► future
       │
       └── function()

为什么要用 shared_ptr?

  • packaged_task 不可拷贝
  • std::function 要求可拷贝
  • shared_ptr 间接解决

七、任务入队流程

{
    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();

并发安全保证

操作是否加锁
检查 stop
插入任务
执行任务

八、析构函数:安全关闭线程池

ThreadPool::~ThreadPool() {
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();
    for (auto &worker : workers) worker.join();
}

析构流程

  1. 设置 stop = true
  2. 唤醒所有线程
  3. 等待线程退出

📌 RAII 思想

对象生命周期 = 资源生命周期


九、线程池的优点

✅ 简洁清晰

✅ 完全 C++11 标准

✅ 异常安全

✅ 支持返回值

✅ 广泛工程可用


十、完整的代码

#ifndef THREAD_POOL_H
#define THREAD_POOL_H

#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>

class ThreadPool {
public:
    // 构造函数:初始化并启动指定数量的工作线程
    explicit ThreadPool(size_t threads);

    // 析构函数:停止所有线程并清理资源
    ~ThreadPool();

    // 核心方法:向线程池提交任务
    // F: 任务函数类型, Args: 参数包
    // 返回值: std::future,用于异步获取任务结果
    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) 
        -> std::future<typename std::result_of<F(Args...)>::type>;

private:
    // 工作线程数组
    std::vector< std::thread > workers;
    
    // 任务队列:存储类型擦除后的 void() 函数对象
    std::queue< std::function<void()> > tasks;
    
    // 同步原语
    std::mutex queue_mutex;             // 互斥锁:保护任务队列
    std::condition_variable condition;  // 条件变量:用于线程间的 等待/唤醒
    bool stop;                          // 停止标志:控制线程池生命周期
};
 

// 构造函数实现
inline ThreadPool::ThreadPool(size_t threads)
    :   stop(false)
{
    // 循环创建工作线程
    for(size_t i = 0; i < threads; ++i)
        workers.emplace_back(
            [this]
            {
                // 每个线程内部是一个死循环,不断尝试获取任务
                for(;;)
                {
                    std::function<void()> task;

                    {
                        // 1. 获取锁,准备访问任务队列
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        
                        // 2. 等待条件满足:
                        // wait 会自动释放锁并阻塞线程,直到被 notify 唤醒。
                        // 唤醒后会检查 lambda 谓词:
                        // 如果 (stop == true) 或者 (任务队列不为空),则停止等待,继续向下执行。
                        // 否则继续阻塞等待。
                        this->condition.wait(lock,
                            [this]{ return this->stop || !this->tasks.empty(); });
                        
                        // 3. 退出判断:
                        // 如果线程池叫停 且 任务队列已空,则该线程结束运行
                        if(this->stop && this->tasks.empty())
                            return;
                        
                        // 4. 取出任务 (使用 std::move 避免拷贝代价)
                        task = std::move(this->tasks.front());
                        this->tasks.pop();
                    } 
                    // 锁在这里自动释放 (lock 离开作用域)

                    // 5. 执行任务 (在锁外执行,保证并发性能)
                    task();
                }
            }
        );
}

// 任务提交函数实现
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) 
    -> std::future<typename std::result_of<F(Args...)>::type>
{
    // 推导任务返回类型
    using return_type = typename std::result_of<F(Args...)>::type;

    // 1. 包装任务:
    // std::bind: 将函数和参数绑定为一个可调用对象
    // std::packaged_task: 包装可调用对象,使其结果可以通过 future 获取
    // std::make_shared: 使用智能指针管理任务,因为 packaged_task 不可拷贝,只能移动,
    // 而 std::function 需要可拷贝对象,所以用 shared_ptr 包装一层。
    auto task = std::make_shared< std::packaged_task<return_type()> >(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );
        
    // 2. 获取 future,以便返回给调用者
    std::future<return_type> res = task->get_future();
    {
        // 3. 加锁访问任务队列 (临界区)
        std::unique_lock<std::mutex> lock(queue_mutex);

        // 如果线程池已停止,禁止提交新任务
        if(stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");

        // 4. 将任务加入队列
        // 这里的 lambda 只是为了适配 queue<function<void()>> 的类型
        // 实际执行时是调用智能指针指向的 packaged_task
        tasks.emplace([task](){ (*task)(); });
    }
    
    // 5. 唤醒一个正在 wait 的工作线程来处理这个任务
    condition.notify_one();
    return res;
}

// 析构函数实现
inline ThreadPool::~ThreadPool()
{
    {
        // 1. 加锁并设置停止标志
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    
    // 2. 唤醒所有线程
    // 它们醒来后会检查 stop 标志。如果队列空了,它们就会 return 退出循环。
    condition.notify_all();
    
    // 3. 等待所有线程执行完毕 (join)
    // 确保主线程不会在子线程结束前退出
    for(std::thread &worker: workers)
        worker.join();
}

#endif

十一、可改进方向(进阶)

改进项说明
使用 std::invoke替代 std::bind
使用 std::invoke_result_t替代 result_of(C++17)
任务优先级priority_queue
动态线程数量auto scaling
工作窃取提升负载均衡
停止策略shutdown_now()