1. 项目概述:为什么我们需要一个“聪明”的线程池?
如果你用C++写过服务器、做过数据处理,或者搞过任何需要榨干CPU性能的项目,大概率都遇到过这样的场景:程序里充满了std::thread,启动时像放烟花一样创建一堆线程,任务来了就扔给它们,任务结束线程也就跟着销毁。乍一看挺爽,性能好像也上去了,但跑久了或者压力一大,问题就全来了:系统资源被频繁的线程创建销毁拖垮,上下文切换开销大得吓人,更头疼的是,你根本不知道什么时候该创建线程,什么时候线程又闲着在空转。
这就是为什么我们需要线程池。它本质上是一个“线程资源管理器”,预先创建好一批线程放在那里待命,形成一个“池子”。当有任务需要执行时,不是临时起线程,而是从池子里唤醒一个空闲线程去处理。任务完成后,线程也不销毁,而是回到池子里等待下一个任务。这个模式解决了资源管理混乱和性能抖动的问题。
但一个“能用”的线程池和一个“好用”的线程池,中间隔着十万八千里。网上随手一搜,能找到几十种C++线程池的实现,很多都停留在“队列+互斥锁+条件变量”的玩具阶段。一旦放到生产环境,面对高并发、任务依赖、优先级调度、异常处理等复杂场景,立马就露馅了。我见过太多项目,初期为了快,随便套了个简单线程池,后期性能瓶颈和诡异的死锁问题排查起来让人头皮发麻。
所以,这次我们不聊怎么造一个“能跑起来”的轮子,而是聚焦于如何设计一个工业级强度、具备智能任务调度能力的C++线程池。我们将从最核心的“生产者-消费者”模型出发,一步步深入到无锁队列、工作窃取、动态扩缩容、优先级调度等高级优化技术,并分享在实际压测和生产部署中踩过的坑和解决之道。无论你是正在学习并发编程,还是需要为你的C++服务找一个可靠的基础组件,这篇文章都能给你提供可直接落地的思路和代码。
2. 核心架构设计:从“队列+锁”到“无锁+窃取”
一个线程池最核心的组件就三样:任务队列、线程组、调度器。最简单的实现,就是一个共享的任务队列(比如std::queue<std::function<void()>>),用一把大锁(std::mutex)保护,配合条件变量(std::condition_variable)来通知线程。工作线程们就在一个循环里等着抢锁、取任务、执行。
这个模型清晰易懂,但在高并发下,锁竞争会成为巨大的瓶颈。所有线程都在争抢同一把锁来取任务,大部分时间可能都花在了等待锁上,CPU利用率根本起不来。
2.1 基础模型:生产者-消费者与资源管理
我们先搭建一个稳健的基础框架。这个框架的核心职责是安全地管理线程生命周期和任务投递。
class ThreadPool { public: explicit ThreadPool(size_t thread_count = std::thread::hardware_concurrency()); ~ThreadPool(); template<typename F, typename... Args> auto enqueue(F&& f, Args&&... args) -> std::future<typename std::invoke_result_t<F, Args...>>; void wait_all(); // 可选:等待所有已提交任务完成 private: void worker_thread(); std::vector<std::thread> workers; // 线程组 std::queue<std::function<void()>> tasks; // 任务队列 std::mutex queue_mutex; // 保护任务队列的互斥锁 std::condition_variable condition; // 用于线程等待/通知的条件变量 bool stop; // 停止标志 };关键点解析:
- 构造与析构:构造函数中根据指定的
thread_count创建所有工作线程,并让它们执行worker_thread函数。析构函数必须优雅关闭:设置stop=true,通知所有条件变量,然后join所有线程。这里必须处理异常,防止线程未join导致程序终止。 - 任务投递(enqueue):这是生产者接口。它需要接受任意可调用对象和参数,将其打包成一个无参数、无返回的
std::function<void()>。更高级的是,它通过std::packaged_task和std::future来支持获取异步结果。这是线程池是否“好用”的第一个门槛。 - 工作线程循环(worker_thread):这是消费者循环。核心逻辑是:加锁 -> 等待条件变量(条件:任务队列非空或池子停止)-> 取任务 -> 解锁 -> 执行任务。这个循环必须非常健壮,能处理任务执行中的异常,避免一个任务崩溃导致整个线程退出。
注意:在这个基础模型中,
enqueue和worker_thread中的任务出队操作都需要锁保护tasks队列。这是性能的第一个瓶颈点。但在优化之前,必须确保这个基础版本是绝对正确和线程安全的,这是所有高级优化的基石。
2.2 进阶架构:引入无锁队列与工作窃取
为了突破锁竞争瓶颈,我们必须对中心化的任务队列动刀。主流优化方向有两个:
1. 无锁任务队列用无锁(lock-free)数据结构替代std::queue和std::mutex。无锁队列通过原子操作(CAS, Compare-And-Swap)实现并发安全,线程在入队和出队时不需要阻塞等待。这对于任务投递极其频繁的场景提升巨大。C++标准库没有提供现成的无锁队列,但我们可以使用第三方库(如moodycamel::ConcurrentQueue)或自己实现一个简单的单生产者单消费者(SPSC)无锁队列。对于线程池,通常需要一个多生产者多消费者(MPMC)队列。
// 示例:使用一个优秀的第三方无锁队列替换原有结构 #include “concurrentqueue.h” // moodycamel::ConcurrentQueue class LockFreeThreadPool { private: moodycamel::ConcurrentQueue<std::function<void()>> tasks; // ... 其他成员 };替换后,enqueue和worker_thread中的出队操作就不再需要std::mutex和std::condition_variable了。但需要注意,无锁队列的“等待”语义需要自己实现,通常可以用std::this_thread::yield()或轻量级信号量来替代条件变量。
2. 工作窃取(Work-Stealing)架构这是更彻底的优化,常用于高性能计算框架(如Java的ForkJoinPool)。其核心思想是去中心化。
- 每个工作线程拥有一个私有的双端任务队列。
- 线程产生的任务优先放入自己的私有队列(队尾入队)。
- 线程执行时,也从自己的私有队列队尾取任务(后进先出,LIFO)。这种局部性有利于缓存命中。
- 当某个线程自己的队列为空时,它不会闲着,而是随机选择另一个线程,从它的队列队头“偷”一个任务来执行(先进先出,FIFO)。
这种架构极大地减少了竞争,因为大部分时候线程只操作自己的队列。只有在自己队列空时,才发生频率较低的“窃取”操作,这时可能需要一些同步开销。
class WorkStealingThreadPool { private: std::vector<std::unique_ptr<WorkStealingQueue>> worker_queues; // 每个线程一个队列 std::vector<std::thread> workers; // ... };实现工作窃取队列需要更精细的同步控制,通常队尾操作(本地线程)可以是无锁或轻锁的,而队头操作(被窃取)则需要更谨慎的同步。
架构选择建议:
- 任务类型单一、投递频繁:优先考虑无锁全局队列。实现相对简单,能有效缓解锁竞争。
- 任务会产生更多子任务(嵌套或递归):工作窃取架构是绝配,能自动实现负载均衡,避免“饥饿”。
- 追求极致性能与复杂度可控:可以结合两者,例如,一个全局无锁队列用于接收外部任务,结合每个线程的本地队列处理自身产生的子任务。
3. 任务调度策略的深度优化
有了高效的队列机制,接下来要让任务被“聪明”地执行。调度策略决定了系统的响应性、公平性和吞吐量。
3.1 优先级调度
不是所有任务都平等。日志刷新可以慢点,但用户请求必须快。我们需要给任务分个三六九等。
实现方案:
多队列优先级:为每个优先级(如高、中、低)维护一个单独的任务队列。调度器(或工作线程)优先从高优先级队列取任务。只有当高优先级队列为空时,才检查中优先级队列,以此类推。
std::array<moodycamel::ConcurrentQueue<Task>, 3> priority_queues; // 索引0为最高优先级挑战:如何防止低优先级任务“饿死”?可以引入“优先级衰减”或时间片轮转策略,例如每执行N个高优先级任务后,强制检查一次低优先级队列。
优先队列(堆):使用一个基于任务优先级的堆(如
std::priority_queue)作为全局队列。这要求任务对象包含优先级字段,并定义比较规则。struct PrioritizedTask { int priority; std::function<void()> func; bool operator<(const PrioritizedTask& other) const { return priority < other.priority; // 值越大优先级越高 } }; std::priority_queue<PrioritizedTask> global_queue;挑战:
std::priority_queue本身不是线程安全的,需要用锁保护,或者寻找支持并发的优先队列实现。此外,堆的插入和删除是O(log n)复杂度,在高频调度时可能成为瓶颈。
实操心得:在生产环境中,我倾向于使用多队列方案。虽然看起来笨,但它的行为更可预测,调试更方便。我们可以为每个优先级队列设置不同的最大长度,当高优先级队列满时,可以降级投递或直接拒绝,这本身就是一种流控手段。而基于堆的方案,在极端情况下所有任务优先级都在动态变化,调试时会非常痛苦。
3.2 动态线程池与负载均衡
固定大小的线程池在流量波峰波谷明显的场景下很吃亏。白天忙时线程不够用,队列堆积;深夜闲时大量线程空转,浪费资源。动态线程池能根据负载自动调整线程数量。
核心指标与策略:
- 负载指标:最常用的是任务队列长度。可以设置两个阈值:
core_pool_size(核心线程数,常驻)和max_pool_size(最大线程数)。 - 扩容策略:当提交新任务时,如果当前活跃线程数 <
core_pool_size,则创建新线程(即使有空闲线程)。如果活跃线程数 >=core_pool_size且任务队列已满,则创建新线程直到达到max_pool_size。这里“队列已满”的判断需要结合优先级队列来考虑,有时只在高优先级队列满时才触发扩容。 - 缩容策略:线程空闲超过
keep_alive_time(如60秒)后,如果当前线程数 >core_pool_size,则终止该线程。实现上,可以在worker_thread循环中,在等待任务时使用条件变量的带超时版本(wait_for),超时后检查自身是否应该退出。
void dynamic_worker_thread(int worker_id) { while (true) { std::function<void()> task; { std::unique_lock<std::mutex> lock(this->queue_mutex); // 使用wait_for,支持超时 bool ok = this->condition.wait_for(lock, std::chrono::seconds(keep_alive_time), [this] { return this->stop || !this->tasks.empty(); }); if (stop && tasks.empty()) return; // 池子关闭 if (!ok) { // 等待超时,说明空闲 if (current_threads.load() > core_pool_size) { // 当前线程数过多,此线程退出 current_threads--; workers[worker_id].detach(); // 从管理列表中移除 return; // 线程函数结束,线程退出 } continue; // 是核心线程,继续等待 } task = std::move(tasks.front()); tasks.pop(); } task(); // 执行任务 } }注意事项:
- 线程创建成本:创建线程是有开销的(主要是内存和系统调用)。动态扩容的阈值和
keep_alive_time需要根据实际业务负载仔细调优,避免在短时间波动下频繁创建销毁线程。 - 缩容时的任务保证:在决定一个空闲线程退出前,必须再次检查全局队列是否真的为空,防止在检查后、退出前的一瞬间有新任务提交,导致任务无人处理。
3.3 任务依赖与有向无环图调度
有些场景下,任务之间不是独立的,而是有依赖关系的。比如,任务B必须在任务A完成后才能开始。这就需要将任务组织成有向无环图。
实现思路:
- 任务封装:定义一个
GraphTask类,包含:实际要执行的函数、一个依赖计数器(初始值为前置任务的数量)、一个后继任务列表。struct GraphTask { std::function<void()> func; std::atomic<int> depend_count{0}; std::vector<GraphTask*> successors; std::mutex succ_mutex; // 保护successors的添加操作 }; - 提交与调度:用户提交任务时,同时指定其依赖的前置任务。线程池内部会建立任务间的图关系。当一个任务(如A)被执行完成后,它会遍历自己的所有后继任务(如B、C),并将它们的
depend_count原子减1。如果某个后继任务的depend_count减到0,说明它的所有依赖都已满足,则将其提交到线程池的可用任务队列中等待调度。 - 线程池的角色:此时的线程池更像一个任务执行器。调度逻辑隐藏在
GraphTask的完成回调中。线程池只需要提供一个可以执行GraphTask的通用接口即可。
挑战与技巧:
- 循环依赖检测:需要在任务图构建阶段进行检测,否则会导致任务永远无法执行。
- 性能:每个任务完成时都需要遍历后继列表并操作原子变量。对于非常细粒度的任务,这个开销可能比任务本身还大。因此,DAG调度更适合粗粒度的、计算密集型的任务。
- 错误传播:如果任务A执行失败,依赖于它的任务B和C应该如何处理?是取消、标记为失败还是继续执行?这需要设计明确的错误处理策略。
4. 生产环境中的“坑”与实战解决方案
理论设计很美好,但真正上线后,各种意想不到的问题才会浮现。下面是我在多个项目中总结的典型问题及解决方法。
4.1 死锁:嵌套提交与资源等待
这是线程池最经典的死锁场景。
场景还原:线程池大小为4。你提交了一个任务A,A在执行过程中,又通过future.get()或wait函数,内部向同一个线程池提交了任务B并等待B的结果。如果此时所有4个线程都在执行类似的任务(都在等待内部提交的子任务完成),而子任务又在队列里等待空闲线程来执行,就会发生死锁——所有线程都在等,没有线程去执行队列里的任务。
解决方案:
- 使用不同的线程池:将可能产生阻塞等待的任务(如IO)和CPU计算任务提交到不同的线程池,隔离风险。
- 线程池感知与逃逸:让任务执行函数知道自己在哪个线程池中执行。当它需要提交一个阻塞性子任务时,如果检测到当前线程池已满或即将死锁,可以将任务“逃逸”到另一个备用线程池,或者直接在当前线程同步执行。
- 设置更大的线程池:简单粗暴但有效,确保线程数远大于可能的最大嵌套深度。但这会浪费资源。
- 使用
std::async:std::async的默认启动策略(std::launch::async | std::launch::deferred)在某些实现中可能会在调用线程上同步执行任务,从而打破死锁,但行为不可移植。
踩坑实录:我们曾有一个图像处理流水线,每个阶段任务都提交到同一个线程池,并在结束时等待下一阶段任务完成。当图片批量处理时,死锁频繁发生。最终我们采用了分层线程池的方案:一个主调度池(线程数少)负责协调,多个工作池(线程数多)负责实际计算,主池任务绝不等待工作池任务,而是通过回调通知,彻底解决了死锁。
4.2 线程局部存储与上下文丢失
很多代码会使用线程局部存储(TLS),比如一些日志框架的线程ID、数据库连接线程绑定、或用户会话上下文。在线程池中,线程是复用的,一个线程执行完用户A的任务后,可能紧接着执行用户B的任务。如果任务依赖TLS中的上下文,就会发生数据错乱。
解决方案:
- 任务初始化时显式传递上下文:将任务执行所需的所有上下文信息,作为参数或类成员,明确地传递给任务函数。这是最清晰、最推荐的方式。
- 在任务执行前后手动设置/清理TLS:在每个任务执行开始时,将当前上下文存入TLS;执行结束后,主动清理TLS。这要求任务包装器来做这件事。
auto wrapped_task = [original_task, context]() { // 1. 保存当前TLS状态(如果需要) // 2. 设置当前TLS为本次任务需要的context set_thread_local_context(context); try { original_task(); } catch (...) { // 3. 无论如何,最后清理TLS clear_thread_local_context(); // 恢复之前保存的TLS状态 throw; } clear_thread_local_context(); // 恢复之前保存的TLS状态 }; pool.enqueue(wrapped_task); - 使用
std::async配合自定义调度器:C++标准没有规定std::async的线程模型,但你可以通过传入自定义的std::launch策略或调度器(这需要编译器或库的支持)来间接控制,但这种方式可移植性差。
4.3 异常处理与资源泄漏
任务在执行过程中可能抛出异常。如果异常没有被捕获并传播到线程函数顶层,会导致std::terminate被调用,整个进程崩溃。
解决方案:
- 在任务包装层进行终极捕获:在
worker_thread调用任务的operator()时,用try-catch(...)包住。try { task(); } catch (const std::exception& e) { // 记录日志:任务执行异常 e.what() // 可以在这里将异常存储到与任务关联的std::promise中 } catch (...) { // 记录日志:任务执行未知异常 } - 与
std::future结合:如果你通过std::packaged_task来获取std::future,那么异常会被自动捕获并存储到future中。当用户调用future.get()时,异常会在用户线程被重新抛出。这是最优雅的方式,将异常处理的责任交还给任务的提交者。 - 资源清理:确保任务中申请的资源(如堆内存、文件句柄、锁)能在异常发生时被正确释放。使用RAII(资源获取即初始化)对象是唯一正确的方法,例如
std::unique_ptr,std::lock_guard。
4.4 性能调优与监控
线程池上线后,如何知道它工作得好不好?
关键监控指标:
- 队列长度:实时监控各个优先级队列的长度。持续堆积意味着消费者(线程)处理能力不足或生产者过快。
- 线程活跃数:当前正在执行任务的线程数 vs. 总线程数。这反映了线程池的利用率。
- 任务执行时间分布:记录每个任务的执行时间,可以统计P50, P90, P99分位值。长尾任务往往是性能瓶颈。
- 拒绝任务数:如果你的线程池实现了“队列满则拒绝”的策略,这个指标能直接反映系统过载情况。
调优参数:
- 核心与最大线程数:
core_pool_size通常设置为CPU核心数,用于处理常规负载。max_pool_size需要根据任务类型(IO密集型还是CPU密集型)和系统资源来设定。IO密集型任务可以设置大很多。 - 队列容量:队列不是越大越好。无界队列可能掩盖问题,导致内存耗尽。有界队列能提供背压(back-pressure),迫使上游系统降速。容量需要根据任务内存占用和系统可承受的延迟来设定。
- 空闲线程存活时间:
keep_alive_time设置过短会导致线程频繁创建销毁;设置过长会导致资源闲置。通常设置在几十秒到几分钟。
一个简单的监控钩子示例:
class MonitoredThreadPool : public ThreadPool { public: struct Metrics { std::atomic<long> tasks_enqueued{0}; std::atomic<long> tasks_completed{0}; std::atomic<int> queue_size{0}; std::atomic<int> active_threads{0}; } metrics; template<typename F, typename... Args> auto enqueue(F&& f, Args&&... args) { metrics.tasks_enqueued++; metrics.queue_size++; // ... 调用父类enqueue,并在任务包装器中更新metrics auto wrapped_func = [this, func = std::bind(std::forward<F>(f), std::forward<Args>(args)...)]() { metrics.active_threads++; metrics.queue_size--; try { func(); } catch (...) { metrics.active_threads--; metrics.tasks_completed++; throw; } metrics.active_threads--; metrics.tasks_completed++; }; // ... 将wrapped_func提交到队列 } };5. 现代C++特性在线程池中的应用
C++11/14/17/20为我们提供了更强大的工具来编写更安全、更高效的线程池。
5.1 使用std::invoke与完美转发
这是实现“万能”enqueue函数的关键。它允许我们接受任何可调用对象和任意数量、类型的参数。
template<typename F, typename... Args> auto enqueue(F&& f, Args&&... args) -> std::future<std::invoke_result_t<F, Args...>> { using return_type = std::invoke_result_t<F, Args...>; // 用std::packaged_task包装任务,以获取future auto task = std::make_shared<std::packaged_task<return_type()>>( std::bind(std::forward<F>(f), std::forward<Args>(args)...) // 更现代的方式是使用lambda捕获完美转发的参数包 // [func = std::forward<F>(f), ...args = std::forward<Args>(args)]() mutable { // return std::invoke(func, args...); // } ); std::future<return_type> res = task->get_future(); { std::lock_guard<std::mutex> lock(queue_mutex); if(stop) { throw std::runtime_error("enqueue on stopped ThreadPool"); } // 将任务包装成void()类型 tasks.emplace([task](){ (*task)(); }); } condition.notify_one(); return res; }std::invoke_result_t在编译时推导调用结果类型,std::forward保持参数的值类别(左值/右值),std::packaged_task负责将任务结果(或异常)传递到std::future。
5.2 使用std::jthread(C++20)
std::jthread在析构时会自动join,避免了传统std::thread因异常导致未join的问题。这可以让线程池的析构逻辑更简洁安全。
// 在ThreadPool的成员变量中 std::vector<std::jthread> workers; // 在构造函数中启动线程 workers.reserve(thread_count); for(size_t i = 0; i < thread_count; ++i) { workers.emplace_back([this] { this->worker_thread(); }); } // 析构函数中,只需设置stop标志并通知,无需显式join。 // workers的析构会自动join所有线程。5.3 使用std::stop_token(C++20) 实现优雅停止
C++20的std::jthread配合std::stop_token,提供了标准化的线程中断机制。我们可以用它来改进线程池的停止逻辑。
void worker_thread(std::stop_token stoken) { while(!stoken.stop_requested()) { std::function<void()> task; { std::unique_lock<std::mutex> lock(queue_mutex); // 条件变量等待时,同时检查停止令牌 condition.wait(lock, [this, &stoken] { return !tasks.empty() || stoken.stop_requested(); }); if(stoken.stop_requested() && tasks.empty()) { return; } task = std::move(tasks.front()); tasks.pop(); } task(); } } // 在ThreadPool的停止函数中 void stop() { stop_source.request_stop(); // stop_source与jthread关联 condition.notify_all(); // 唤醒所有等待线程 // jthread析构时会自动join }这种方式比手动管理bool stop标志更安全、更标准。
5.4 使用std::atomic与内存序
在线程池的无锁或高性能区域,正确使用std::atomic和内存序至关重要。例如,在实现动态线程计数时:
std::atomic<int> active_threads{0}; std::atomic<int> total_threads{0}; void worker_thread() { active_threads.fetch_add(1, std::memory_order_relaxed); // ... 执行任务 active_threads.fetch_sub(1, std::memory_order_relaxed); }对于简单的计数器,std::memory_order_relaxed通常就足够了,因为它只保证原子性,不提供同步语义,性能最好。但如果这个计数器用于同步逻辑(如“当active_threads==0时做某事”),则需要更强的内存序,如std::memory_order_acq_rel。
6. 从零构建一个简易但健壮的线程池:代码实战
理论说了这么多,我们动手写一个融合了部分优化思想的、健壮的基础线程池。这个版本包含:优雅停止、异常安全、future返回、以及基本的动态线程管理思想。
#include <vector> #include <queue> #include <memory> #include <thread> #include <mutex> #include <condition_variable> #include <future> #include <functional> #include <stdexcept> #include <atomic> class SimpleRobustThreadPool { public: explicit SimpleRobustThreadPool(size_t min_threads, size_t max_threads = 0, size_t max_queue_size = 1000) : min_threads_(min_threads) , max_threads_(max_threads == 0 ? std::thread::hardware_concurrency() * 2 : max_threads) , max_queue_size_(max_queue_size) , stop_(false) { if (min_threads_ == 0 || max_threads_ < min_threads_) { throw std::invalid_argument("Invalid thread count arguments"); } for (size_t i = 0; i < min_threads_; ++i) { workers_.emplace_back([this] { this->worker_loop(); }); } current_threads_.store(min_threads_); } ~SimpleRobustThreadPool() { { std::lock_guard<std::mutex> lock(queue_mutex_); stop_ = true; } condition_.notify_all(); for (std::thread &worker : workers_) { if (worker.joinable()) { worker.join(); } } } template<class F, class... Args> auto enqueue(F&& f, Args&&... args) -> std::future<typename std::invoke_result_t<F, Args...>> { using return_type = typename std::invoke_result_t<F, Args...>; 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"); } // 动态扩容检查:如果队列快满了,且还能创建新线程,就创建 if (tasks_.size() >= max_queue_size_ * 0.8 // 队列使用率阈值 && current_threads_.load() < max_threads_) { size_t new_threads = std::min(max_threads_ - current_threads_.load(), static_cast<size_t>(2)); // 一次最多创建2个 for (size_t i = 0; i < new_threads; ++i) { workers_.emplace_back([this] { this->worker_loop(); }); current_threads_.fetch_add(1); } } // 如果队列已满,拒绝任务(这里简单抛出异常,生产环境可定义更丰富的策略) if (tasks_.size() >= max_queue_size_) { throw std::runtime_error("ThreadPool task queue is full"); } tasks_.emplace([task](){ (*task)(); }); } condition_.notify_one(); return res; } size_t get_queue_size() const { std::lock_guard<std::mutex> lock(queue_mutex_); return tasks_.size(); } size_t get_current_threads() const { return current_threads_.load(); } private: void worker_loop() { while(true) { std::function<void()> task; { std::unique_lock<std::mutex> lock(queue_mutex_); // 等待条件:有任务或线程池停止 condition_.wait(lock, [this]{ return stop_ || !tasks_.empty(); }); // 如果停止且任务为空,线程退出 if(stop_ && tasks_.empty()) { current_threads_.fetch_sub(1); return; } // 取任务 task = std::move(tasks_.front()); tasks_.pop(); } // 执行任务,捕获所有异常防止线程退出 try { task(); } catch (...) { // 这里可以记录日志,但不应抛出异常导致线程终止 // std::cerr << "Task execution threw an exception." << std::endl; } } } // 线程管理 std::vector<std::thread> workers_; std::atomic<size_t> current_threads_{0}; const size_t min_threads_; const size_t max_threads_; // 任务队列 std::queue<std::function<void()>> tasks_; const size_t max_queue_size_; // 同步 mutable std::mutex queue_mutex_; std::condition_variable condition_; bool stop_; };这个线程池的特点与使用示例:
- 基础功能完整:支持任意任务、返回future、优雅停止。
- 简单的动态扩容:当队列长度达到阈值(80%)且未达最大线程数时,会自动创建新线程(每次最多2个)。
- 有界队列与拒绝策略:队列有最大长度限制,满时会抛出异常,这是一种简单的背压机制。
- 异常安全:工作线程循环内捕获所有异常,确保单个任务失败不会导致线程崩溃退出。
- 使用方法:
SimpleRobustThreadPool pool(4, 16); // 最小4线程,最大16线程,队列最大1000 // 提交一个任务并获取future auto future = pool.enqueue([](int a, int b) { std::this_thread::sleep_for(std::chrono::seconds(1)); return a + b; }, 10, 20); // 做其他事情... int result = future.get(); // 等待并获取结果 std::cout << "Result: " << result << std::endl; // 输出 30
这个实现仍然使用全局锁,因此不适合极限性能场景,但它结构清晰、功能稳健,是学习线程池原理和进行项目初期开发的绝佳起点。要将其升级为生产级别,你需要根据前面章节的讨论,逐步引入无锁队列、工作窃取、优先级调度等高级特性。记住,在并发编程中,正确性永远优于性能,在确保代码绝对健壮的基础上,再进行有针对性的优化。