C++线程池工业级实现:从核心原理到避坑指南
1. 项目概述
最近在后台收到不少朋友的私信,想让我聊聊线程池的C++实现。这确实是个老生常谈但又常谈常新的话题,尤其是在高并发、高性能的服务端开发中,一个设计得当的线程池往往是系统稳定性的基石。很多人觉得线程池不就是个“任务队列加几个线程”吗,网上代码一搜一大把。但真到了生产环境,你会发现魔鬼全在细节里:任务堆积了怎么办?线程异常退出怎么处理?如何优雅地关闭而不丢数据?这些坑,没踩过几次是写不出健壮代码的。
今天,我就结合自己这些年从零搭建和维护多个高并发系统的经验,手把手带你从零实现一个工业级可用的C++线程池。我们不止于“能跑”,更要追求“跑得稳”、“管得好”。我会把代码掰开揉碎了讲,重点放在那些容易被忽略但至关重要的设计决策和避坑指南上。无论你是刚接触多线程的初学者,还是想优化现有架构的资深工程师,相信都能从中找到有用的东西。
2. 线程池的核心价值与设计目标
在动手写代码之前,我们必须先想清楚:为什么要用线程池?它到底解决了什么问题?一个清晰的设计目标,是后续所有编码工作的灯塔。
2.1 为什么我们需要线程池
最直接的原因就是成本。在操作系统层面,线程的创建和销毁是非常“重”的操作。它涉及内核对象的分配、内存栈的建立、上下文信息的初始化等一系列系统调用。如果一个服务对每个 incoming request 都new std::thread,那么在请求密集时,大量的时间会浪费在线程的生命周期管理上,而不是真正处理业务逻辑。
线程池通过“池化”技术,预先创建好一批线程并保持它们存活。当有任务到来时,直接从池中分配一个空闲线程去执行,任务完成后线程回归池中等待下一次分配。这就好比一个公司常年雇佣一批核心员工(线程),有项目(任务)来了就分配下去,项目结束员工就待命,避免了频繁招聘(创建)和裁员(销毁)的开销。
但它的价值远不止于此。一个设计良好的线程池还能:
- 控制并发度:避免无限制创建线程导致系统资源(如内存、CPU时间片)被耗尽,这是防止服务雪崩的重要阀门。
- 统一管理:提供了任务提交、执行、结果获取、异常处理的统一入口,简化了并发编程的复杂度。
- 提供可观察性:可以方便地监控当前活跃任务数、队列长度、线程状态等指标,为系统调优提供数据支持。
2.2 一个工业级线程池的设计目标
基于上述价值,我们这次要实现的线程池,应该瞄准以下几个具体目标:
功能完备性:
- 任务提交:支持任意可调用对象(函数、Lambda、成员函数等)。
- 结果获取:支持异步获取任务执行结果或异常。
- 生命周期管理:支持安全、优雅的启动、停止和资源回收。
- 基础配置:允许设置线程数量、任务队列容量等。
健壮性与安全性:
- 线程安全:所有对共享资源(任务队列、状态标志)的访问必须正确同步。
- 异常安全:任务执行抛出的异常不应导致线程池崩溃,应能传递给调用者。
- 资源泄漏防护:确保在任何退出路径下(正常停止、异常中断),线程都能正确
join,资源得以释放。 - 死锁预防:仔细设计锁的获取顺序和条件变量的使用,避免经典死锁场景。
性能与可扩展性:
- 低延迟提交:任务提交操作应尽可能快,不能成为瓶颈。
- 避免忙等待:空闲线程应通过条件变量休眠,而非空转消耗CPU。
- 合理的默认配置:例如,默认线程数通常设置为
std::thread::hardware_concurrency()(CPU核心数)或稍多一些,以充分利用CPU资源。 - 可观测性:预留接口或内部状态,便于监控运行指标。
明确了这些目标,我们的代码就有了清晰的边界和评判标准。接下来,我们就进入核心实现环节。
3. 核心数据结构与类设计
实现线程池,本质上是设计几个核心组件并管理好它们之间的协作关系。我们先来定义线程池类的骨架和关键数据结构。
3.1 线程池类骨架与成员变量
我们首先定义一个ThreadPool类。它的私有成员变量构成了线程池的“五脏六腑”:
#include <vector> #include <queue> #include <thread> #include <mutex> #include <condition_variable> #include <future> #include <functional> #include <atomic> #include <memory> class ThreadPool { public: explicit ThreadPool(size_t thread_num = std::thread::hardware_concurrency()); ~ThreadPool(); // 禁止拷贝构造和赋值 ThreadPool(const ThreadPool&) = delete; ThreadPool& operator=(const ThreadPool&) = delete; // 提交任务的通用接口 template<class F, class... Args> auto submit(F&& f, Args&&... args) -> std::future<decltype(f(args...))>; void shutdown(); // 优雅关闭 void shutdown_now(); // 立即关闭 private: // 工作线程的主循环函数 void worker_loop(); // 内部任务类型定义 using Task = std::function<void()>; // 成员变量 std::vector<std::thread> workers_; // 工作线程容器 std::queue<Task> tasks_; // 任务队列 std::mutex queue_mutex_; // 保护任务队列的互斥锁 std::condition_variable condition_; // 用于线程同步的条件变量 std::condition_variable condition_producer_; // 生产者条件变量(用于队列满时等待) std::atomic<bool> stop_{false}; // 停止标志(立即停止) std::atomic<bool> graceful_stop_{false}; // 优雅停止标志(处理完队列任务再停) std::atomic<size_t> pending_tasks_{0}; // 等待+执行中的任务计数 size_t max_queue_size_{0}; // 任务队列最大容量,0表示无限制 };关键成员解析:
workers_和tasks_:这是核心。workers_存放所有工作线程对象,tasks_是待执行的任务队列。我们使用std::queue是因为它满足 FIFO(先进先出)的简单调度需求。queue_mutex_:这是保证线程安全的“门神”。任何对tasks_队列的读写操作(push,pop,empty,size)都必须先锁住它。condition_和condition_producer_:这是线程间的“通信员”。condition_:消费者(工作线程)等待的条件。当任务队列为空时,工作线程在此等待,直到有任务被提交(notify_one/notify_all)或线程池停止。condition_producer_:生产者(提交任务的线程)等待的条件。当任务队列满时,提交任务的线程在此等待,直到有工作线程取走任务腾出空间。
stop_和graceful_stop_:两个原子布尔标志,用于控制线程池的停止行为。这是实现不同关闭策略的关键。stop_:为true时,所有线程立即退出,不再处理新任务,队列中的任务也可能被丢弃。graceful_stop_:为true时,线程池不再接受新任务,但会继续处理完队列中所有已存在的任务后再退出。
pending_tasks_:原子计数器,用于追踪“已提交但未完成”的任务总数(包括在队列中等待的和正在执行的)。这对于实现优雅关闭和监控非常有用。max_queue_size_:任务队列的容量上限。这是一个重要的流量控制参数,防止任务无限制堆积导致内存耗尽。
注意:为什么需要两个条件变量?这是为了避免“虚假唤醒”和实现更精细的控制。如果只用
condition_,当队列满时,生产者线程无法被有效阻塞和唤醒。使用两个条件变量,可以让生产者和消费者在不同的条件上等待,逻辑更清晰,也避免了用while循环检查复杂条件的开销。
3.2 任务封装与结果传递机制
任务提交后,我们如何获取它的执行结果或捕获异常?答案是std::packaged_task和std::future。这是我们线程池的“异步返回值快递系统”。
在submit函数内部,我们是这样封装一个任务的:
template<class F, class... Args> auto ThreadPool::submit(F&& f, Args&&... args) -> std::future<decltype(f(args...))> { // 推导任务返回类型 using return_type = decltype(f(args...)); // 关键:使用 std::packaged_task 包装用户任务 // std::packaged_task<return_type()> 是一个可调用对象,它调用时返回 return_type // 我们使用 std::bind 将函数 f 和参数 args... 绑定成一个无参可调用对象 auto task_ptr = std::make_shared<std::packaged_task<return_type()>>( std::bind(std::forward<F>(f), std::forward<Args>(args)...) ); // 从 packaged_task 获取 future,用于后续获取结果 std::future<return_type> result_future = task_ptr->get_future(); // 准备一个 void() 类型的任务,放入队列 // 这个 Lambda 的核心是执行 (*task_ptr)(),即执行用户真正的函数 Task wrapper_task = [task_ptr]() { (*task_ptr)(); }; // ... (将 wrapper_task 放入任务队列的代码,后续讲解) return result_future; }原理解析:
std::packaged_task:它是一个类模板,包装了一个可调用对象,并允许异步获取该对象的调用结果。它内部关联了一个std::future。std::bind:这里的作用是将用户传入的函数f和参数包args...“打包”成一个新的、无参数的可调用对象。这是因为我们队列里存储的Task类型是std::function<void()>,它不接受参数。std::shared_ptr:为什么用智能指针?因为std::packaged_task是不可拷贝的(拷贝构造函数被删除),但我们需要将它捕获到 Lambda 表达式中(按值捕获)。使用std::shared_ptr可以安全地共享这个任务对象的所有权。std::future:调用packaged_task::get_future()获得一个与任务结果关联的future对象。调用者可以通过future::get()阻塞等待直到任务完成,并获取返回值或重新抛出任务中产生的异常。
一个常见的坑:参数传递方式注意std::bind(std::forward<F>(f), std::forward<Args>(args)...)。这里使用了完美转发std::forward来保持参数的值类别(左值/右值)。但是,std::bind默认会按值拷贝其参数。如果你希望传递引用,必须使用std::ref或std::cref明确包装。
int x = 42; // 错误:submit 内部的 bind 会拷贝 x,Lambda 修改的是副本 auto fut1 = pool.submit([](int& a) { a = 100; }, x); // 正确:使用 std::ref 传递引用包装器 auto fut2 = pool.submit([](int& a) { a = 100; }, std::ref(x));4. 核心实现:线程管理与任务调度
有了清晰的数据结构,接下来就是让它们动起来。这部分是线程池的“发动机”,也是最容易出并发 bug 的地方。
4.1 构造函数与线程启动
构造函数负责初始化成员变量并启动指定数量的工作线程。
ThreadPool::ThreadPool(size_t thread_num, size_t max_queue_size) : max_queue_size_(max_queue_size) { // 参数校验 if (thread_num == 0) { thread_num = std::thread::hardware_concurrency(); if (thread_num == 0) thread_num = 2; // 硬件并发数未知时的保底值 } workers_.reserve(thread_num); // 预分配空间,避免多次扩容 for (size_t i = 0; i < thread_num; ++i) { // 创建线程,并立即执行 worker_loop 成员函数 workers_.emplace_back(&ThreadPool::worker_loop, this); } }关键点:
std::thread::hardware_concurrency():返回硬件支持的并发线程数,通常是 CPU 逻辑核心数。这是一个合理的默认值起点。workers_.reserve(thread_num):这是一个性能优化小技巧。提前分配好vector的内存,避免在emplace_back过程中因容量不足导致多次重新分配和拷贝。std::thread(&ThreadPool::worker_loop, this):创建线程,并指定其入口点为ThreadPool::worker_loop成员函数,同时传递this指针作为对象上下文。
4.2 工作线程的主循环 (worker_loop)
这是每个工作线程一生都在执行的函数,是线程池的“心脏”。
void ThreadPool::worker_loop() { while (true) { Task task; { // 1. 获取队列锁 std::unique_lock<std::mutex> lock(queue_mutex_); // 2. 等待条件成立:有任务可执行,或收到停止信号 // 这个 Lambda 是等待的“谓词”(predicate),防止虚假唤醒 condition_.wait(lock, [this]() { // 如果要求立即停止,则退出等待(线程将结束) // 如果要求优雅停止但队列为空,也退出等待(线程将结束) // 否则,只有当队列非空时才退出等待 return stop_.load() || (graceful_stop_.load() && tasks_.empty()) || !tasks_.empty(); }); // 3. 检查退出条件 // 情况A: 立即停止,无论队列是否为空,直接退出 if (stop_.load() && tasks_.empty()) { return; } // 情况B: 优雅停止且队列已空,退出 if (graceful_stop_.load() && tasks_.empty()) { return; } // 4. 从队列中取出一个任务 task = std::move(tasks_.front()); tasks_.pop(); --pending_tasks_; // 待处理任务数减1 // 5. 如果队列之前是满的,取出任务后就有空间了,通知可能等待的生产者 if (max_queue_size_ > 0 && tasks_.size() == max_queue_size_ - 1) { condition_producer_.notify_all(); } } // 锁在这里释放,允许其他线程操作队列 // 6. 执行任务!注意,执行任务时不持有锁,这是关键! try { task(); } catch (...) { // 任务执行异常,我们在此处捕获并忽略(异常已通过future传递) // 但可以在这里记录日志 // LOG_ERROR("Task execution failed with unknown exception."); } } }逐行解析与避坑指南:
- 锁的粒度:我们使用
std::unique_lock而不是std::lock_guard,因为我们需要在条件变量上等待,而wait操作会暂时释放锁。 - 条件等待的谓词:
condition_.wait(lock, predicate)是标准用法。wait会在阻塞前先检查predicate,如果为true则直接继续,避免“虚假唤醒”问题。我们的谓词逻辑清晰地定义了线程何时应该被唤醒。 - 退出条件检查:在
wait返回后,必须重新检查退出条件。因为wait可能因为stop_或graceful_stop_被设置为true而返回。这里区分了两种停止模式。 - 任务转移:使用
std::move将队列头部的任务移动到局部变量task中。这避免了不必要的拷贝(如果Task内部持有大量资源)。 - 生产者通知:这是实现有界队列的关键。当从满队列中取走一个任务后,队列不再满,我们使用
condition_producer_.notify_all()唤醒所有可能因为队列满而阻塞的submit调用。 - 任务执行在锁外:这是最重要的设计原则之一!任务
task()的执行必须在锁的作用域之外。否则,如果一个任务执行时间很长,它会一直持有queue_mutex_,导致其他所有线程都无法从队列中取任务,也无法提交新任务,线程池的并发能力就完全丧失了。
4.3 任务提交 (submit) 的完整实现
现在我们把submit函数补全,看看生产者一侧的逻辑。
template<class F, class... Args> auto ThreadPool::submit(F&& f, Args&&... args) -> std::future<decltype(f(args...))> { using return_type = decltype(f(args...)); // 1. 创建 packaged_task 和 future auto task_ptr = std::make_shared<std::packaged_task<return_type()>>( std::bind(std::forward<F>(f), std::forward<Args>(args)...) ); std::future<return_type> result_future = task_ptr->get_future(); // 2. 准备包装任务 Task wrapper_task = [task_ptr]() { (*task_ptr)(); }; { // 3. 获取队列锁 std::unique_lock<std::mutex> lock(queue_mutex_); // 4. 如果设置了队列上限且队列已满,则等待 if (max_queue_size_ > 0) { condition_producer_.wait(lock, [this]() { // 等待条件:停止标志为真,或者队列未满 // 如果线程池已停止,则不再接受任务,抛出异常 if (stop_.load() || graceful_stop_.load()) { throw std::runtime_error("ThreadPool is stopped, cannot submit new task."); } return tasks_.size() < max_queue_size_; }); } else { // 无队列限制,但也要检查是否已停止 if (stop_.load() || graceful_stop_.load()) { throw std::runtime_error("ThreadPool is stopped, cannot submit new task."); } } // 5. 将任务放入队列 tasks_.emplace(std::move(wrapper_task)); ++pending_tasks_; // 待处理任务数加1 } // 锁释放 // 6. 通知一个等待的消费者线程(工作线程) condition_.notify_one(); // 7. 返回 future 给调用者 return result_future; }关键实现细节:
- 有界队列的等待逻辑:如果
max_queue_size_ > 0,在队列满时,submit会阻塞在condition_producer_.wait上。这实现了背压(Backpressure),调用者会感知到系统繁忙,而不是无限制地消耗内存。 - 停止状态的检查:在准备放入任务前,无论队列状态如何,都必须检查
stop_和graceful_stop_标志。如果线程池已进入停止状态,应拒绝新任务并抛出异常,这是一种 fail-fast 的设计。 - 通知策略:我们使用
condition_.notify_one()。这意味着每次放入一个任务,只唤醒一个工作线程。这比notify_all()更高效,避免了“惊群效应”(所有线程被唤醒去争抢一个任务)。当然,在特定场景下(如任务优先级很高),你也可以选择notify_all()。 - 异常传递:如果用户任务在执行中抛出异常,这个异常会被
std::packaged_task捕获并存储。当调用者调用result_future.get()时,这个异常会在调用者线程中重新抛出。因此,线程池的工作线程worker_loop中的try-catch(...)块主要是为了记录日志,防止异常逃逸导致线程崩溃,而不是处理业务异常。
5. 优雅关闭与资源清理
线程池的关闭,尤其是优雅关闭,是检验其健壮性的“试金石”。处理不好,轻则任务丢失,重则程序卡死或崩溃。
5.1 两种关闭策略的实现
我们提供了两种关闭接口:shutdown_now()(立即停止)和shutdown()(优雅停止)。
void ThreadPool::shutdown_now() { // 1. 设置立即停止标志 stop_.store(true); { // 2. 清空任务队列(立即停止意味着丢弃未执行的任务) std::lock_guard<std::mutex> lock(queue_mutex_); // 这里可以遍历队列,对未执行的 packaged_task 做一些清理(如设置异常) // 简单实现是直接清空 while (!tasks_.empty()) { tasks_.pop(); --pending_tasks_; } } // 3. 唤醒所有等待的线程(消费者和生产者) condition_.notify_all(); condition_producer_.notify_all(); // 4. 等待所有工作线程结束 for (auto& worker : workers_) { if (worker.joinable()) { worker.join(); } } workers_.clear(); } void ThreadPool::shutdown() { // 1. 设置优雅停止标志,不再接受新任务 graceful_stop_.store(true); // 2. 唤醒所有可能因为队列空而等待的工作线程 condition_.notify_all(); // 3. 等待所有已提交的任务完成 // 这里我们通过 pending_tasks_ 计数器来判断 // 注意:这个循环在 pending_tasks_ 不为0时,会短暂释放CPU while (pending_tasks_.load() > 0) { std::this_thread::yield(); // 让出CPU时间片,避免忙等待 } // 4. 此时队列已空,所有任务已完成。设置立即停止标志,确保工作线程退出循环 stop_.store(true); condition_.notify_all(); // 再次通知,确保所有线程都能看到 stop_ 的变化 // 5. 等待所有工作线程结束 for (auto& worker : workers_) { if (worker.joinable()) { worker.join(); } } workers_.clear(); } ThreadPool::~ThreadPool() { // 析构函数默认采用立即停止策略,确保资源被释放 if (!stop_.load()) { shutdown_now(); } }两种策略的对比与应用场景:
| 特性 | shutdown_now()(立即停止) | shutdown()(优雅停止) |
|---|---|---|
| 行为 | 立即设置停止标志,清空任务队列,唤醒所有线程并等待它们退出。 | 先设置优雅停止标志(拒新纳旧),等待队列中现有任务全部执行完毕,再停止线程。 |
| 任务处理 | 丢弃所有队列中未执行的任务。 | 保证执行所有已提交到队列的任务。 |
| 适用场景 | 程序需要立即退出,不关心未完成任务的后果(如快速崩溃恢复、测试环境)。 | 服务正常关闭,需要保证所有已接受请求都被处理完(如收到SIGTERM信号时)。 |
| 实现关键 | 需要清空队列并正确递减pending_tasks_计数器。 | 需要等待pending_tasks_降为0,注意避免忙等待。 |
5.2 析构函数的责任
析构函数~ThreadPool()必须保证资源被正确释放。我们选择调用shutdown_now()作为默认行为。这是因为在对象生命周期结束时,通常意味着程序不再需要这个池子,丢弃未完成任务是可接受的。如果用户需要优雅停止,应该在析构前显式调用shutdown()。
一个重要的坑:join()与线程析构std::thread对象在析构时,如果线程仍是joinable的(即尚未调用join()或detach()),程序会调用std::terminate()导致崩溃。因此,在shutdown_now和shutdown中,我们必须遍历workers_,对每个可连接的线程调用join()。调用join()后,std::thread对象变为不可连接状态,其析构才是安全的。
6. 生产环境常见问题与进阶优化
一个基础的线程池跑起来后,要真正用于生产环境,还需要考虑很多边界情况和性能优化。下面是我在实践中总结的几个关键问题和解决方案。
6.1 线程池的“八股文”面试题与实战答案
很多面试官喜欢问线程池的参数和问题,这里结合我们的实现给出答案:
核心线程数、最大线程数、队列容量怎么设置?
- 我们的实现:目前是固定线程数。更高级的线程池(如Java的
ThreadPoolExecutor)有核心线程和最大线程的概念,允许在任务暴增时创建新线程。 - 实战策略:
- CPU密集型任务:线程数 ≈ CPU核心数。过多线程会导致频繁的上下文切换,降低性能。我们的默认值
hardware_concurrency()就是基于此。 - I/O密集型任务:线程数可以远大于CPU核心数,因为线程大部分时间在等待I/O(如网络、磁盘)。公式可以是
核心数 * (1 + 平均等待时间/平均计算时间)。 - 队列容量:需要权衡。队列太长会消耗内存,增加任务延迟;队列太短容易触发拒绝策略。通常需要根据业务可接受的延迟和系统内存来设定。我们的
max_queue_size_就是这个参数。
- CPU密集型任务:线程数 ≈ CPU核心数。过多线程会导致频繁的上下文切换,降低性能。我们的默认值
- 我们的实现:目前是固定线程数。更高级的线程池(如Java的
任务队列满了怎么办?(拒绝策略)
- 我们的实现:
submit函数会阻塞,直到队列有空位。这是一种Caller-Runs的变体(调用者线程被阻塞)。 - 其他常见策略:
- AbortPolicy(中止):直接抛出异常,如
std::runtime_error。 - DiscardPolicy(丢弃):默默丢弃新任务。
- DiscardOldestPolicy(丢弃最旧):丢弃队列头部的任务,然后尝试提交新任务。
- CallerRunsPolicy(调用者运行):由提交任务的线程自己执行该任务。
- AbortPolicy(中止):直接抛出异常,如
- 如何实现:可以在
submit函数的队列满判断分支中,根据预设的枚举值来选择不同的行为。
- 我们的实现:
线程池中的线程异常退出怎么办?
- 问题:如果工作线程在执行任务时发生未捕获的异常(非通过
packaged_task抛出的),比如段错误,线程会崩溃。池子里的线程数就少了。 - 解决方案:在
worker_loop的最外层加一个try-catch(...),捕获所有异常。一旦捕获到,记录致命日志,然后重启一个线程替代崩溃的线程。这需要更复杂的管理逻辑,比如维护一个“线程句柄”列表和对应的状态。
- 问题:如果工作线程在执行任务时发生未捕获的异常(非通过
6.2 性能优化与高级特性实现思路
避免锁竞争:使用无锁队列或双端队列
- 问题:我们的实现中,所有线程(生产者和消费者)共用一个
queue_mutex_。在高并发下,这可能成为瓶颈。 - 优化方案:
- 无锁队列:如
boost::lockfree::queue或自己实现一个基于 CAS 的无锁队列。这能极大提升并发性能,但实现复杂。 - 双端队列与工作窃取:每个工作线程维护一个本地双端队列。线程优先从自己队列的头部取任务(LIFO,利于缓存)。当自己队列空时,随机从其他线程队列的尾部“窃取”任务。这是
work-stealing算法,被许多高性能线程库(如 Intel TBB)采用,能很好地利用缓存局部性并减少锁竞争。
- 无锁队列:如
- 问题:我们的实现中,所有线程(生产者和消费者)共用一个
支持任务优先级
- 需求:有些任务比另一些更重要,需要优先执行。
- 实现:将
std::queue<Task>替换为优先队列,如std::priority_queue<Task>。Task需要包含优先级字段,并定义比较规则。提交任务时指定优先级。注意,这可能会引起“优先级反转”或“饥饿”问题,需要仔细设计。
增加监控接口
- 需求:运维需要知道线程池的实时状态。
- 实现:增加一些
get方法,返回原子计数器的值。
size_t get_pending_task_count() const { return pending_tasks_.load(); } size_t get_queue_size() const { std::lock_guard<std::mutex> lock(queue_mutex_); return tasks_.size(); } size_t get_active_thread_count() const { /* 需要额外计数器 */ }
6.3 一个完整的、带基础拒绝策略的示例
让我们扩展之前的submit函数,增加一个简单的拒绝策略枚举。
enum class RejectPolicy { BLOCKING, // 阻塞提交者(默认) ABORT, // 抛出异常 DISCARD_NEW // 静默丢弃新任务 }; template<class F, class... Args> auto ThreadPool::submit(F&& f, Args&&... args, RejectPolicy policy = RejectPolicy::BLOCKING) -> std::future<decltype(f(args...))> { // ... 创建 task_ptr 和 result_future 的代码同上 ... bool task_submitted = false; { std::unique_lock<std::mutex> lock(queue_mutex_); // 检查停止状态 if (stop_.load() || graceful_stop_.load()) { throw std::runtime_error("ThreadPool is stopped."); } // 检查队列是否已满 if (max_queue_size_ > 0 && tasks_.size() >= max_queue_size_) { switch (policy) { case RejectPolicy::BLOCKING: condition_producer_.wait(lock, [this]() { return tasks_.size() < max_queue_size_ || stop_.load() || graceful_stop_.load(); }); // 被唤醒后需要重新检查状态,因为可能在等待期间线程池停止了 if (stop_.load() || graceful_stop_.load()) { throw std::runtime_error("ThreadPool is stopped while waiting."); } break; case RejectPolicy::ABORT: throw std::runtime_error("ThreadPool task queue is full."); case RejectPolicy::DISCARD_NEW: // 返回一个特殊的 future,表示任务被丢弃 // 这里可以返回一个已经就绪的、包含特定错误码的 future std::promise<return_type> prom; prom.set_exception(std::make_exception_ptr( std::runtime_error("Task discarded due to full queue"))); return prom.get_future(); default: break; } } // 队列未满或阻塞后有空位,提交任务 tasks_.emplace(std::move(wrapper_task)); ++pending_tasks_; task_submitted = true; } if (task_submitted) { condition_.notify_one(); return result_future; } // 对于 DISCARD_NEW,上面已经返回了 future throw std::logic_error("Should not reach here."); }这个扩展使得线程池的鲁棒性更强,能够应对不同的负载场景。实现一个线程池就像打磨一件工具,基础功能是锤头锤柄,而这些细节的打磨决定了它是一把顺手的好锤子,还是一把容易伤到自己的钝器。希望这篇长文能帮你打造出属于你自己的、趁手可靠的并发工具。
