一、先来看线程池的内部私有成员

    //线程池内部数据结构
    //这里使用shared_ptr共享,确保线程池销毁后线程仍能够安全访问(计数不为0,不会释放资源)
    struct Pool
    {
        std::mutex mtx;                          //互斥锁
        std::condition_variable cond;            //条件变量
        std::atomic<bool> isClosed{false};       //原子关闭标志
        std::queue<std::function<void()>> tasks; //任务队列
    };

    std::shared_ptr<Pool> pool_; //线程池内部数据的共享指针

首先是结构体Pool,里面存储了互斥锁,可以单独上锁也可以配合条件变量使用。线程池停止标志isClosed,默认是false,这里不用atomic也可以,原因和析构函数、工作线程的wait逻辑有关,后面会详细说明。保留atomic<bool>也不会出错,只是会带来极其微小的性能开销,对绝大多数场景来说可以忽略不计。用一个queue来存储function<void()>类型的无参无返回值任务函数。

注意:这里isClosed不可以使用小括号()赋值。在 C++ 语法中规定,类成员声明中不能使用()直接初始化,这会被编译器误解为成员函数声明,推荐使用列表初始化{}方式。

因为线程池的线程被设为detach分离状态,需要保证线程池关闭后线程也可以正常使用资源。所以使用共享指针pool_,保证线程池销毁后,只要还有工作线程持有这个指针,Pool结构体的资源就不会被释放(强引用计数未归零)。


二、再来看线程池构造函数

    //创建指定数量的工作线程
    explicit ThreadPool(size_t threadCount = 8) : pool_(std::make_shared<Pool>())
    {
        //默认8个线程,传入make_shared<Pool>指针
        assert(threadCount > 0);

        //创建工作线程
        for (size_t i = 0; i < threadCount; i++)
        {
            std::thread([pool = pool_] {                        //拷贝一份共享指针
                std::unique_lock<std::mutex> locker(pool->mtx); //上锁
                while (true)
                { //进入线程循环
                    if (!pool->tasks.empty())
                    {
                        //从队列中取出任务
                        auto task = std::move(pool->tasks.front()); //移动避免拷贝
                        pool->tasks.pop();
                        locker.unlock();
                        //解锁后执行任务,避免长时间持有锁
                        try{
                             task();
                        }catch(...){
                            //记录日志或忽略,不能让异常穿透
                        }
                        locker.lock();
                    }
                    else if (pool->isClosed)
                    {
                        //线程池已经关闭,退出线程
                        break;
                    }
                    else
                    {
                        //等待新任务
                        pool->cond.wait(locker, [pool] {
                            return !pool->tasks.empty() || pool->isClosed;
                        }); //谓词函数,防止虚假唤醒
                        //这里调用wait时,会释放锁,直到被唤醒重新获取锁
                    }
                }
            })
                .detach(); //分离线程,自动回收资源
        }
    }
   
    // 禁止拷贝构造和拷贝赋值(必须加!否则会崩溃)
    ThreadPool(const ThreadPool&) = delete;
    ThreadPool& operator=(const ThreadPool&) = delete;

    //生成默认移动构造函数和移动赋值运算符
    ThreadPool(ThreadPool &&) = default;
    ThreadPool& operator=(ThreadPool&&) = default;

提供了一个带默认值的有参构造函数,不传入参数的情况下默认 8 个线程,同时给共享指针pool_初始化。

创建指定数量的工作线程并调用detach,通过 lambda 表达式值捕获共享指针pool_使引用计数 + 1。内部用unique_lock上锁后进入工作线程的主循环。

  • 任务队列不为空:使用move取出队首任务(避免拷贝消耗,此时队首元素被移动后处于移后源状态,值未指定),先解锁再执行任务,避免长时间持有锁阻塞其他线程,执行完成后再上锁。
  • 线程池停止标志为true:直接退出线程。
  • 暂时没有任务:通过条件变量cond阻塞等待新任务,设置谓词函数防止虚假唤醒。在调用wait时,会自动释放锁,直到被唤醒后重新获取锁资源。

同时必须禁止拷贝构造和拷贝赋值,否则两个线程池对象会共享同一个内部Pool状态,析构时会重复执行关闭逻辑导致崩溃。

explicit用法

注意:这里有参构造函数前面带有explicit关键字,作用是禁止构造函数的隐式类型转换。

当构造函数只有一个参数时(threadCount,且带默认值),C++ 默认允许隐式类型转换:

// 不加explicit,这种代码会编译通过
ThreadPool pool = 10; 
// 编译器偷偷把10隐式转换成ThreadPool(10)

使用explicit后,只能显式调用构造函数,代码更安全:

// 正确(显式调用)
ThreadPool pool(10);
ThreadPool pool{10};

explicit 只能用单参数构造函数(最常见),多参数构造函数,类型转换函数。其他函数加 explicit 直接编译失败。

而且 explicit 必须写在【类内声明】的位置,explicit 绝对不能写在【类外实现】的位置。

因为线程池构造函数是单参数 + 默认值,属于 C++ 隐式转换的高危场景,去掉explicit后,ThreadPool pool = 10;会被编译器偷偷允许,最终可能会导致以下后果

1. 代码可读性差
// 不加explicit,编译通过,但没人看得懂
ThreadPool pool = 10; 

这里的的线程池 10 代表工作线程数量,但这种写法完全隐藏了语义:

  • 别人会以为是赋值、枚举、错误码,完全看不出是创建 10 个线程的线程池
  • 对比显式写法 ThreadPool pool(10);,后者一眼就能对应构造函数逻辑。
2. 触发临时对象隐式构造, detach() 线程直接泄露 / 异常退出

线程池核心设计:线程用 detach() 分离 + 共享 pool_ 指针 + 析构时关闭线程池

// 线程创建后直接分离
std::thread(...).detach();
// 析构函数会标记 isClosed = true
~ThreadPool() {
    if(pool_) {
        std::lock_guard<std::mutex> locker(pool_->mtx);
        pool_->isClosed = true;
    }
    pool_->cond.notify_all();
}

不加 explicit 会出现这种恐怖代码:

// 隐式构造【临时线程池】,创建10个detach线程
// 这行代码执行完,临时对象立刻销毁!
ThreadPool pool = 10; 

执行流程(完全破坏线程池逻辑)

  1. 隐式构造临时 ThreadPool → 创建 10 个 detach 线程;
  2. 临时对象立刻调用析构函数 → 设置 isClosed = true → 唤醒所有线程;
  3. 10 个线程刚启动就收到关闭信号,直接退出;
  4. detach 线程无法被主动管理,造成线程资源泄漏、逻辑完全失效
3. 误赋值覆盖原有线程池,shared_ptr<Pool> 彻底失控

线程池用 std::shared_ptr<Pool> 管理核心资源,不加 explicit

// 正常创建8线程池
ThreadPool pool(8);  

// 手滑写了赋值语句,不加explicit:编译通过!
pool = 10; 

后果

  • 编译器用 10 隐式构造临时线程池
  • 赋值操作覆盖原有的 pool_ 共享指针;
  • 原来的 8 个 detach 线程彻底丢失管理,变成野线程;
  • 新临时线程池析构,关闭新线程,整个线程池彻底混乱。
4. 绕过编译期检查,触发 assert(threadCount > 0) 运行时崩溃

线程池的代码里有严格的线程数校验:

assert(threadCount > 0);
  • explicitThreadPool pool = 0;直接编译报错,提前拦截错误;
  • 不加 explicitThreadPool pool = 0;编译通过,运行时直接触发断言崩溃。

甚至传负数、无效整数,都会编译通过,直到运行时才崩溃。

5. 函数参数误传,凭空创建线程池

如果在其他业务代码里有函数接收 ThreadPool 对象:

// 业务函数
void startWork(ThreadPool pool) {
    pool.AddTask([]{ /* 任务 */ });
}

// 不加explicit,手滑传了整数,编译通过!
startWork(5); 

后果

  • 编译器偷偷用 5 构造一个全新的临时线程池
  • 凭空创建 5 个系统线程,执行完就销毁;
  • 完全违背业务逻辑,多线程程序行为彻底失控。

三、下面来看线程池析构函数

    //析构函数,关闭线程池
    ~ThreadPool()
    {
        //static_cast显式转换
        if (static_cast<bool>(pool_))
           {
            {
                std::lock_guard<std::mutex> locker(pool_->mtx);
                pool_->isClosed = true;
            }
                 pool_->cond.notify_all();
           }
 
    }

这里使用了static_cast显式转换,这是为了更规范(不使用也行)。

{}控制锁的范围,在里面使用lock_guard上锁后再将停止标志设为true,最后唤醒所有线程。

注意:虽然isClosed设为了atomic,它自身是线程安全的,但是直接给它赋值会造成其他问题。

关于条件变量wait的执行逻辑:

  • 如果谓词为truewait不释放锁,立即返回,locker仍处于锁定状态。
  • 如果谓词为falsewait原子地解锁并把线程挂起。此时锁被释放,其他线程可以拿到锁。被唤醒后,它会重新拿到锁,然后再次检查谓词,返回时锁是锁定的。

如果析构线程不拿同一把锁就去改isClosed并通知,就可能发生在 "解锁后、挂起前" 的间隙里,造成唤醒丢失。所以析构函数必须上锁后再赋值isClosed


四、最后是添加任务函数

    //添加任务到线程池队列中
    template<class F>
    void AddTask(F&& task){//&&右值引用,既可以传左值也可以传右值
        {
            std::lock_guard<std::mutex> locker(pool_->mtx);
            pool_->tasks.emplace(std::forward<F>(task));
        }//在队尾构造一个对象,避免拷贝消耗。forward完美转发,保证原有task是右值传入,还是右值,不会被转为左值
        pool_->cond.notify_one();
    }

使用模板,可以传入任意类型的可调用对象,包括普通函数、Lambda 表达式、std::bind绑定的函数、类成员函数、仿函数。

通过F&&万能引用接收任意值类型的任务,&&不光能接收右值任务,也可以接收左值任务,配合后面的forward使用,避免不必要的拷贝开销。

添加任务时上锁,通过emplace在容器内直接构造对象,避免拷贝。通过forward完美转发,保证任务的原始值类型(还原任务本来的样子,传进来是啥样,构造时就是啥样)。

如果直接写emplace(task),那么即使传入的是右值,在函数内部task作为有名字的变量会变成左值,emplace传入队列时就只能进行拷贝操作。


完整代码

#ifndef THREADPOOL_H
#define THREADPOOL_H

#include <cassert>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <thread>
#include <functional>
#include <atomic>

//固定大小的线程池类
class ThreadPool
{
public:
    //创建指定数量的工作线程
    explicit ThreadPool(size_t threadCount = 8) : pool_(std::make_shared<Pool>())
    {
        //默认8个线程,传入make_shared<Pool>指针
        assert(threadCount > 0);

        //创建工作线程
        for (size_t i = 0; i < threadCount; i++)
        {
            std::thread([pool = pool_] {                        //拷贝一份共享指针
                std::unique_lock<std::mutex> locker(pool->mtx); //上锁
                while (true)
                { //进入线程循环
                    if (!pool->tasks.empty())
                    {
                        //从队列中取出任务
                        auto task = std::move(pool->tasks.front()); //移动避免拷贝
                        pool->tasks.pop();
                        locker.unlock();
                        //解锁后执行任务,避免长时间持有锁
                        try{
                             task();
                        }catch(...){
                            //记录日志或忽略,不能让异常穿透
                        }
                        locker.lock();
                    }
                    else if (pool->isClosed)
                    {
                        //线程池已经关闭,退出线程
                        break;
                    }
                    else
                    {
                        //等待新任务
                        pool->cond.wait(locker, [pool] {
                            return !pool->tasks.empty() || pool->isClosed;
                        }); //谓词函数,防止虚假唤醒
                        //这里调用wait时,会释放锁,直到被唤醒重新获取锁
                    }
                }
            })
                .detach(); //分离线程,自动回收资源
        }
    }
   
    // 禁止拷贝构造和拷贝赋值(必须加!否则会崩溃)
    ThreadPool(const ThreadPool&) = delete;
    ThreadPool& operator=(const ThreadPool&) = delete;

    //生成默认移动构造函数和移动赋值运算符
    ThreadPool(ThreadPool &&) = default;
    ThreadPool& operator=(ThreadPool&&) = default;

    //析构函数,关闭线程池
    ~ThreadPool()
    {
        //static_cast显式转换
        if (static_cast<bool>(pool_))
           {
            {
                std::lock_guard<std::mutex> locker(pool_->mtx);
                pool_->isClosed = true;
            }
                 pool_->cond.notify_all();
           }
 
    }


    //添加任务到线程池队列中
    template<class F>
    void AddTask(F&& task){//&&右值引用,既可以传左值也可以传右值
        {
            std::lock_guard<std::mutex> locker(pool_->mtx);
            pool_->tasks.emplace(std::forward<F>(task));
        }//在队尾构造一个对象,避免拷贝消耗。forward完美转发,保证原有task是右值传入,还是右值,不会被转为左值
        pool_->cond.notify_one();
    }

private:
    //线程池内部数据结构
    //这里使用shared_ptr共享,确保线程池销毁后线程仍能够安全访问(计数不为0,不会释放资源)
    struct Pool
    {
        std::mutex mtx;                          //互斥锁
        std::condition_variable cond;            //条件变量
        std::atomic<bool> isClosed{false};       //原子关闭标志
        std::queue<std::function<void()>> tasks; //任务队列
    };

    std::shared_ptr<Pool> pool_; //线程池内部数据的共享指针
};

#endif

更多推荐