news 2026/7/15 9:22:23

从零实现C++线程池:深入理解多线程编程核心原理与实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零实现C++线程池:深入理解多线程编程核心原理与实践

1. 项目概述:为什么我们需要亲手实现一个C++线程池?

在C++高并发编程的世界里,线程池是一个绕不开的核心组件。你可能在面试中被问过它的七个参数,也可能在生产环境中遇到过因为线程管理不当导致的性能瓶颈或诡异崩溃。网上有很多现成的库,比如Boost.Asio的io_context,或者C++17之后的std::async配合策略,但“拿来就用”和“自己实现一遍”的体验是天差地别的。自己动手实现一个线程池,不仅仅是完成一个编程练习,更是深入理解多线程编程的同步、互斥、任务调度、资源管理等核心概念的绝佳途径。它能让你在面对“线程池生产环境会发生的问题以及解决”这类问题时,不再停留在背诵八股文的层面,而是能从设计者的角度,清晰地知道问题可能出在哪里,以及如何精准地定位和修复。

这个项目,我们将从零开始,使用现代C++(C++11及以上)的标准库,一步步构建一个功能完整、健壮可用的线程池。我们会涵盖从最基础的线程创建与管理,到任务队列的设计,再到优雅关闭、异常处理等高级话题。过程中,我会穿插那些在文档里不会写的“踩坑”经验和调试技巧。无论你是正在啃《C++并发编程实战》的学生,还是工作中需要优化服务性能的开发者,这个手把手的实现过程都将为你提供扎实的实践基础。最终,你将得到一个可以直接嵌入到你个人项目(无论是c++小游戏服务器端,还是数据处理后端)中的工具,并真正理解其每一行代码背后的考量。

2. 核心设计思路与架构拆解

在动手写代码之前,我们必须想清楚几个关键问题:线程池由哪些核心部分组成?它们之间如何协作?我们的设计目标是什么?

2.1 线程池的四大核心组件

一个典型的线程池,无论语言是Java还是C++,其核心架构都大同小异,主要由以下四个部分协同工作:

  1. 任务队列(Task Queue):这是线程池的“待办事项清单”。所有提交过来的任务(通常是可调用对象,如函数、lambda表达式、std::function)都被放入这个队列中等待执行。它必须是线程安全的,因为多个生产者(提交任务的线程)和多个消费者(工作线程)会同时访问它。
  2. 工作线程组(Worker Threads):这是一组预先创建好的、处于休眠或等待状态的线程。它们唯一的使命就是不断地从任务队列中取出任务并执行。线程的数量通常是固定的(固定大小线程池),也可以是动态调整的(弹性线程池)。
  3. 同步机制(Synchronization Primitives):这是协调线程工作的“交通灯”和“信号枪”。核心包括:
    • 互斥锁(Mutex):保护任务队列等共享资源,防止多线程同时修改导致数据竞争。
    • 条件变量(Condition Variable):用于线程间的等待和通知。当任务队列为空时,工作线程通过条件变量进入等待状态,避免忙等待消耗CPU;当有新任务提交时,通知等待的线程。
  4. 管理接口(Management Interface):提供给外部使用的API,主要是提交任务的函数(如submit,enqueue)和控制线程池生命周期的函数(如shutdown,wait_for_all)。

2.2 我们的设计目标与选型考量

对于这个教学兼实用型的实现,我们设定以下目标:

  • 简洁清晰:代码结构直观,便于理解和教学。
  • 现代C++:充分利用C++11/14/17的特性,如std::functionstd::packaged_taskstd::future、lambda表达式、移动语义等,避免手动管理原始指针和内存。
  • 线程安全:核心数据结构(任务队列)的访问必须是原子的,确保在多线程环境下行为正确。
  • 优雅关闭:能够安全地停止所有线程,并处理完队列中剩余的任务,避免任务丢失或线程无法退出的问题。
  • 结果获取:支持提交任务后获取其执行结果(通过std::future)。

为什么不用std::async直接替代?std::async确实简单,但它默认的启动策略(std::launch::async | std::launch::deferred)由实现定义,可能不会立即创建线程,且对线程生命周期和数量的控制力很弱。而一个手写的线程池,给了我们完全的掌控权,这对于需要稳定、可预测性能的生产环境至关重要。

3. 基础实现:一个最小可用的线程池

让我们从最简单的版本开始,逐步添加功能。首先,我们需要一个线程安全的任务队列。

3.1 构建线程安全的任务队列

任务队列的核心是一个std::queue,但直接使用它是非线程安全的。我们需要用互斥锁(std::mutex)来保护它,并用条件变量(std::condition_variable)来实现线程间的等待-通知机制。

这里,我们使用一个类型别名来定义任务:using Task = std::function<void()>;。这意味着一个任务可以是任何无参数、无返回值的可调用对象。对于需要返回值的任务,我们稍后会通过std::packaged_task进行包装。

#include <queue> #include <mutex> #include <condition_variable> #include <functional> class ThreadSafeQueue { public: // 入队一个任务 void enqueue(Task task) { { std::lock_guard<std::mutex> lock(m_mutex); m_queue.push(std::move(task)); } m_cond.notify_one(); // 通知一个等待中的工作线程 } // 尝试从队头取出一个任务(非阻塞) bool try_dequeue(Task& task) { std::lock_guard<std::mutex> lock(m_mutex); if (m_queue.empty()) { return false; } task = std::move(m_queue.front()); m_queue.pop(); return true; } // 阻塞等待并从队头取出一个任务 bool wait_and_dequeue(Task& task) { std::unique_lock<std::mutex> lock(m_mutex); // 等待条件:队列非空或要求停止等待(用于线程池关闭) m_cond.wait(lock, [this]() { return !m_queue.empty() || m_stopWaiting; }); if (m_queue.empty() && m_stopWaiting) { return false; // 队列空且被要求停止,返回false通知线程退出 } task = std::move(m_queue.front()); m_queue.pop(); return true; } // 通知所有等待线程停止等待(用于关闭线程池) void stop_waiting() { { std::lock_guard<std::mutex> lock(m_mutex); m_stopWaiting = true; } m_cond.notify_all(); } bool empty() const { std::lock_guard<std::mutex> lock(m_mutex); return m_queue.empty(); } private: mutable std::mutex m_mutex; std::queue<Task> m_queue; std::condition_variable m_cond; bool m_stopWaiting = false; };

关键点解析与避坑指南:

  • 锁的粒度:在enqueuetry_dequeue中,我们使用std::lock_guard,它会在构造时加锁,析构时自动解锁,非常适合作用域明确的场景。在wait_and_dequeue中,我们使用std::unique_lock,因为它需要配合条件变量,在等待时会暂时释放锁。
  • 条件变量的谓词m_cond.wait(lock, predicate)中的predicate(lambda表达式)是防止“虚假唤醒”的关键。线程被唤醒后,会检查谓词条件是否真正满足(队列非空或要求停止),如果不满足则继续等待。
  • 移动语义std::move(task)的使用避免了不必要的拷贝。任务对象(尤其是捕获了大量变量的lambda)可能很大,移动操作效率更高。
  • stop_waiting标志:这是实现优雅关闭的关键。当线程池需要关闭时,我们设置此标志并通知所有等待线程,它们检查到标志后就会退出循环,而不是永远等待新任务。

3.2 实现工作线程与线程池骨架

有了任务队列,我们就可以创建线程池类了。它的核心是:在构造函数中创建指定数量的工作线程,每个线程都运行一个循环,不断地从任务队列中取任务并执行。

#include <vector> #include <thread> #include <atomic> #include <iostream> class ThreadPool { public: explicit ThreadPool(size_t threadCount = std::thread::hardware_concurrency()) : m_done(false) { try { for (size_t i = 0; i < threadCount; ++i) { m_threads.emplace_back(&ThreadPool::worker_thread, this); } } catch (...) { // 如果创建线程失败,需要设置停止标志并等待已创建的线程结束 m_done = true; m_taskQueue.stop_waiting(); for (auto& t : m_threads) { if (t.joinable()) t.join(); } throw; // 重新抛出异常 } } ~ThreadPool() { shutdown(); } // 提交一个无返回值的任务 void submit(Task task) { m_taskQueue.enqueue(std::move(task)); } // 关闭线程池,等待所有任务完成 void shutdown() { if (m_done.exchange(true)) { return; // 防止重复调用 } m_taskQueue.stop_waiting(); // 通知所有工作线程停止等待 for (auto& thread : m_threads) { if (thread.joinable()) { thread.join(); } } } private: // 每个工作线程的执行函数 void worker_thread() { while (!m_done) { Task task; if (m_taskQueue.wait_and_dequeue(task)) { try { task(); // 执行任务 } catch (...) { // 异常处理:生产环境中需要更精细的日志记录 std::cerr << "Exception occurred in worker thread." << std::endl; } } else { // wait_and_dequeue 返回 false,意味着收到停止信号且队列已空,退出循环 break; } } } std::vector<std::thread> m_threads; ThreadSafeQueue m_taskQueue; std::atomic<bool> m_done; // 原子布尔标志,指示线程池是否应停止 };

使用示例:

int main() { ThreadPool pool(4); // 创建包含4个工作线程的线程池 // 提交10个任务 for (int i = 0; i < 10; ++i) { pool.submit([i]() { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "Task " << i << " executed by thread " << std::this_thread::get_id() << std::endl; }); } // 主线程等待一段时间,观察输出 std::this_thread::sleep_for(std::chrono::seconds(2)); pool.shutdown(); // 析构函数也会调用shutdown return 0; }

实操心得与注意事项:

  1. 线程数量设置:默认使用std::thread::hardware_concurrency()是个好习惯,它返回硬件支持的并发线程数(通常是CPU核心数)。但对于I/O密集型任务,线程数可以适当多于核心数。
  2. 构造函数异常安全:在构造函数中创建线程时,如果中途抛出异常(例如系统资源不足),我们必须清理已经创建成功的线程,否则会导致资源泄漏。这就是try-catch块的作用。
  3. 析构函数与shutdown:将shutdown逻辑放在析构函数中,遵循RAII原则,确保线程池对象销毁时资源被正确释放。m_done使用std::atomic确保多线程下的可见性和原子性。
  4. 工作线程中的异常捕获:任务执行可能抛出异常。在工作线程内部捕获异常并记录日志(或根据业务逻辑处理)至关重要,避免异常逃逸导致整个工作线程意外终止。在我们的简单实现中只是打印了错误信息,生产环境需要集成到日志系统。

4. 功能增强:支持获取任务返回值

上面的线程池只能提交void()类型的任务。在实际应用中,我们经常需要获取任务的执行结果。这时就需要用到std::packaged_taskstd::future

std::packaged_task包装一个可调用对象,并允许异步获取其结果(通过与之关联的std::future)。我们需要修改任务队列和提交接口。

4.1 升级任务队列以支持std::packaged_task

由于std::packaged_task是模板类,我们需要一个类型擦除的包装器。一个巧妙的方法是使用一个返回voidstd::packaged_task,其内部包装了实际有返回值的任务。但更通用的做法是定义一个基类,然后使用派生类模板。

为了保持代码相对简洁,我们采用另一种常见模式:在任务队列中存储一个返回voidstd::function,但在提交时,将std::packaged_task包装成一个void()的lambda,并在这个lambda中执行packaged_task,同时将关联的std::future返回给调用者。

首先,修改ThreadSafeQueue,使其存储std::function<void()>(即之前的Task)。这不需要改变队列本身。

然后,在ThreadPool中添加一个新的提交函数模板:

#include <future> #include <utility> class ThreadPool { public: // ... 其他成员保持不变 ... // 提交一个有返回值的任务,返回一个std::future template<typename F, typename... Args> auto submit(F&& f, Args&&... args) -> std::future<decltype(f(args...))> { // 推导返回类型 using return_type = decltype(f(args...)); // 创建一个packaged_task,绑定函数和参数 // packaged_task本身不可拷贝,需要用shared_ptr管理 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> future_result = task_ptr->get_future(); // 包装任务:创建一个void()的lambda,在其中执行packaged_task Task wrapper_task = [task_ptr]() { (*task_ptr)(); }; // 将包装后的任务入队 m_taskQueue.enqueue(std::move(wrapper_task)); // 返回future给调用者 return future_result; } // 原有的void submit(Task task) 可以保留,用于提交无返回值的任务 void submit(Task task) { m_taskQueue.enqueue(std::move(task)); } private: // ... worker_thread 等保持不变 ... };

原理与技巧解析:

  1. 完美转发std::forward<F>(f)std::forward<Args>(args)...确保了传入的函数对象和参数保持其原始的值类别(左值/右值),提高效率。
  2. std::bind:将可调用对象f和参数包args...绑定在一起,生成一个新的无参数的可调用对象,供std::packaged_task使用。
  3. std::shared_ptr管理packaged_taskstd::packaged_task是不可拷贝的,但可以被移动。为了能将其捕获到lambda中(lambda按值捕获),我们需要用std::shared_ptr来共享其所有权。这样,即使原始的task_ptr离开作用域,lambda中持有的智能指针仍然有效。
  4. 类型擦除:核心技巧在于wrapper_task这个lambda。它的类型是std::function<void()>,完美匹配我们的任务队列。而在其内部,它通过智能指针调用了std::packaged_task,从而间接执行了用户提交的原始任务并设置了结果值。

使用增强版线程池:

int main() { ThreadPool pool; // 提交一个有返回值的任务 auto future1 = pool.submit([]() -> int { std::this_thread::sleep_for(std::chrono::milliseconds(500)); return 42; }); // 提交另一个任务 auto future2 = pool.submit([](int a, int b) -> int { return a + b; }, 10, 20); // 在主线程中获取结果(会阻塞直到任务完成) int result1 = future1.get(); int result2 = future2.get(); std::cout << "Result 1: " << result1 << std::endl; // 输出 42 std::cout << "Result 2: " << result2 << std::endl; // 输出 30 pool.shutdown(); return 0; }

5. 生产环境级优化与问题解决

一个基础的线程池跑起来不难,但要用于生产环境,我们必须考虑更多边界情况和性能优化。下面就是那些在“线程池生产环境会发生的问题以及解决”中常被提及的要点。

5.1 优雅关闭与剩余任务处理

我们之前的shutdown实现是“温和”的:设置停止标志,通知线程,等待它们执行完队列中已有的任务后退出。但有时我们可能需要更激进的“立即关闭”模式,即丢弃所有未开始执行的任务。

实现“立即关闭”模式:我们可以增加一个stop方法,并在ThreadSafeQueue中添加清空队列的功能。

class ThreadSafeQueue { public: // ... 其他方法 ... void clear() { std::lock_guard<std::mutex> lock(m_mutex); while (!m_queue.empty()) { m_queue.pop(); } } }; class ThreadPool { public: // ... 其他方法 ... // 温和关闭:等待所有已提交任务完成 void shutdown() { if (m_done.exchange(true)) return; m_taskQueue.stop_waiting(); for (auto& t : m_threads) { if (t.joinable()) t.join(); } } // 立即关闭:丢弃所有未执行的任务 void stop() { if (m_done.exchange(true)) return; m_taskQueue.clear(); // 清空任务队列 m_taskQueue.stop_waiting(); for (auto& t : m_threads) { if (t.joinable()) t.join(); } } };

注意clear()操作会丢弃任务。如果这些任务持有重要资源或承诺,可能会导致问题。生产环境中,可能需要根据任务优先级或类型来决定是丢弃还是等待。

5.2 线程池的动态扩缩容

固定大小的线程池简单,但可能无法适应负载波动。我们可以实现一个简单的动态线程池:当任务队列长度超过阈值时,增加线程;当线程空闲时间过长时,减少线程。

核心思路:

  1. 维护一个“核心线程数”和“最大线程数”。
  2. 除了核心线程常驻外,额外线程作为“临时工”,在空闲一段时间后自动退出。
  3. 需要一个管理者线程(或由提交任务的线程兼职)来监控队列长度和线程状态。

由于实现较为复杂,这里给出一个简化的概念框架:

class DynamicThreadPool { std::vector<std::thread> m_workers; ThreadSafeQueue m_tasks; std::atomic<bool> m_done{false}; std::atomic<int> m_busyCount{0}; // 忙碌线程计数 size_t m_coreThreads; size_t m_maxThreads; std::chrono::milliseconds m_keepAliveTime; void manager_thread() { while (!m_done) { std::this_thread::sleep_for(std::chrono::seconds(1)); // 定期检查 size_t queueSize = /* 获取队列近似大小 */; size_t currentThreads = m_workers.size(); if (queueSize > currentThreads && currentThreads < m_maxThreads) { // 任务多,线程少,且未达上限,创建新线程 add_worker(); } else if (queueSize == 0 && currentThreads > m_coreThreads) { // 任务少,线程多,且超过核心数,可以考虑回收空闲线程 // 需要更复杂的机制判断哪些是“空闲”的临时线程 } } } void worker_thread(bool isTemporary) { auto idleSince = std::chrono::steady_clock::now(); while (!m_done) { Task task; if (m_tasks.wait_and_dequeue_for(task, m_keepAliveTime)) { // 需要实现带超时的等待 m_busyCount++; try { task(); } catch (...) { /* ... */ } m_busyCount--; idleSince = std::chrono::steady_clock::now(); // 重置空闲计时 } else { // 超时,意味着在keepAliveTime内没拿到任务 if (isTemporary && (std::chrono::steady_clock::now() - idleSince) > m_keepAliveTime) { // 临时线程空闲超时,退出循环,线程结束 break; } } } } };

避坑指南:动态线程池的复杂性

  • 线程创建与销毁成本:频繁创建销毁线程开销很大,动态调整的阈值和策略需要仔细调优,否则可能适得其反。
  • 状态同步:管理者线程需要获取队列大小、忙碌线程数等信息,这些都需要原子操作或锁保护,增加了复杂度。
  • “临时线程”标识:需要一种机制来区分核心线程和临时线程,以便正确管理其生命周期。通常可以为每个线程关联一个状态标志。
  • 实践建议:对于大多数应用,一个经过精心调优的固定大小线程池往往比一个简单的动态线程池更稳定、性能更好。只有在负载波动极大且可预测性差的场景下,才值得引入动态扩缩容的复杂性。Java的ThreadPoolExecutor提供了丰富的策略,其实现也相当复杂,可以作为高级参考。

5.3 任务优先级调度

默认的FIFO(先进先出)队列可能不满足所有需求。有时我们需要高优先级的任务被优先执行。这可以通过使用优先队列(如std::priority_queue)替代std::queue来实现。

实现要点:

  1. 定义任务结构体,包含任务函数和优先级数值。
  2. 修改ThreadSafeQueue内部容器为std::priority_queue,并自定义比较函数。
  3. 入队时指定优先级。
struct PrioritizedTask { int priority; Task task; // 优先级高的先出队(注意:priority_queue默认是最大堆,priority大的在前) bool operator<(const PrioritizedTask& other) const { return priority < other.priority; // 数值小的优先级低 } }; class PriorityThreadSafeQueue { // 内部使用 std::priority_queue<PrioritizedTask> // 相应地修改 enqueue, dequeue 等方法,接受或返回 PrioritizedTask };

注意事项:优先级调度可能导致“饥饿”问题,即低优先级任务永远得不到执行。在生产环境中,可能需要更复杂的调度算法,如时间片轮转与优先级结合。

5.4 异常处理与资源泄漏预防

这是生产环境稳定性的生命线。

  1. 任务中的异常:如前所述,必须在工作线程的循环内部捕获任务抛出的异常。除了记录日志,有时还需要将异常传递回提交任务的线程。这可以通过std::promisestd::future来实现:在包装任务时,不仅设置结果值,也设置异常。

    // 在submit函数内部,包装任务时捕获异常并设置到promise中 auto task_ptr = std::make_shared<std::packaged_task<return_type()>>( [f = std::forward<F>(f), args...]() -> return_type { try { return f(args...); } catch (...) { // 获取当前任务的promise并设置异常 // 注意:这需要更精细的设计来获取对应的promise std::rethrow_exception(std::current_exception()); } } );

    更常见的做法是,让调用者自己在std::future::get()时处理异常。我们的简单实现采用了后者。

  2. 线程池析构时的任务状态:确保在析构函数中,所有线程都已正确join。我们的shutdown方法已经做到了这一点。要避免线程还未结束就销毁线程池对象,那会导致std::terminate被调用。

  3. 死锁预防:确保锁的获取顺序一致。在我们的简单实现中,锁只保护任务队列,顺序是固定的。但如果线程池内部有更复杂的交互(例如动态调整线程数),就需要仔细设计锁的层次。

6. 性能调优与高级特性探讨

当线程池用于高性能场景时,以下几个点值得深入。

6.1 避免锁竞争:无锁队列与工作窃取

当线程数很多(比如几十上百)时,所有工作线程都去竞争任务队列这一把锁,会成为性能瓶颈。两个高级优化方向是:

  • 无锁队列(Lock-free Queue):使用原子操作(CAS)实现队列的入队和出队,彻底消除锁开销。但实现极其复杂,且并非在所有场景下都比有锁队列快(特别是在低竞争时)。通常建议使用成熟的第三方库,如moodycamel::ConcurrentQueue
  • 工作窃取(Work Stealing):这是现代高性能线程池(如Java的ForkJoinPool)的核心思想。每个工作线程维护一个自己的本地任务队列。提交任务时,优先放入提交者线程的本地队列。工作线程也优先从自己的本地队列取任务。只有当本地队列为空时,才去“窃取”其他线程队列中的任务。这大大减少了全局竞争。实现一个正确且高效的工作窃取线程池是一个不小的挑战。

6.2 与std::asyncstd::jthread的对比与结合

  • std::async:如前所述,它更像一个高级的任务启动器,隐藏了线程管理的细节,但控制力弱。我们的手写线程池提供了更强的可控性和可预测性。
  • std::jthread(C++20):这是一个可联结(joinable)的线程,其析构函数会自动join,并且支持协作式中断请求(通过request_stop())。在我们的线程池实现中,可以用std::jthread替代std::thread,并利用其停止令牌(std::stop_token)来实现更优雅的线程中断,替代我们自定义的m_done标志。这能让代码更符合现代C++的风格。

6.3 线程局部存储(Thread Local Storage)的应用

如果任务中频繁使用某个资源(比如随机数生成器、内存分配器、数据库连接),且该资源的初始化成本较高,可以考虑使用线程局部存储(thread_local)。这样每个工作线程都拥有自己的一个实例,避免了每次任务都创建或加锁访问共享资源。

class ThreadPool { private: void worker_thread() { // 每个线程初始化自己的随机数引擎 thread_local std::mt19937 rng(std::random_device{}()); while (!m_done) { Task task; if (m_taskQueue.wait_and_dequeue(task)) { task(); // 任务内部可以安全地使用 `rng`,无需加锁 } else { break; } } } };

7. 完整代码示例与集成测试

将上述所有特性(基础线程池、返回值、优雅关闭)整合,并添加一些简单的测试,形成一个相对完整的版本。由于篇幅限制,这里给出一个整合后的核心框架,省略部分重复代码。

// ThreadSafeQueue.h #pragma once #include <queue> #include <mutex> #include <condition_variable> #include <functional> class ThreadSafeQueue { public: using Task = std::function<void()>; void enqueue(Task task); bool wait_and_dequeue(Task& task); void stop_waiting(); bool empty() const; void clear(); // 可选,用于立即关闭 private: mutable std::mutex m_mutex; std::queue<Task> m_queue; std::condition_variable m_cond; bool m_stopWaiting = false; }; // ThreadPool.h #pragma once #include "ThreadSafeQueue.h" #include <vector> #include <thread> #include <future> #include <atomic> class ThreadPool { public: explicit ThreadPool(size_t threadCount = std::thread::hardware_concurrency()); ~ThreadPool(); // 禁止拷贝和赋值 ThreadPool(const ThreadPool&) = delete; ThreadPool& operator=(const ThreadPool&) = delete; // 提交无返回值任务 void submit(ThreadSafeQueue::Task task); // 提交有返回值任务 (C++17 可使用 auto 推导简化) template<typename F, typename... Args> auto submit(F&& f, Args&&... args) -> std::future<decltype(f(args...))>; void shutdown(); // 温和关闭 void stop(); // 立即关闭 (需要Queue支持clear) private: void worker_thread(); std::vector<std::thread> m_workers; ThreadSafeQueue m_tasks; std::atomic<bool> m_done{false}; }; // ThreadPool.cpp (部分实现) ThreadPool::ThreadPool(size_t threadCount) { // ... 创建线程,异常安全处理 ... } ThreadPool::~ThreadPool() { shutdown(); } void ThreadPool::worker_thread() { while (!m_done) { ThreadSafeQueue::Task task; if (m_tasks.wait_and_dequeue(task)) { try { task(); } catch (...) { // 记录日志 } } else { break; } } } template<typename F, typename... Args> auto ThreadPool::submit(F&& f, Args&&... args) -> std::future<decltype(f(args...))> { using return_type = decltype(f(args...)); 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> res = task_ptr->get_future(); m_tasks.enqueue([task_ptr]() { (*task_ptr)(); }); return res; } void ThreadPool::submit(ThreadSafeQueue::Task task) { m_tasks.enqueue(std::move(task)); } void ThreadPool::shutdown() { if (m_done.exchange(true)) return; m_tasks.stop_waiting(); for (auto& t : m_workers) { if (t.joinable()) t.join(); } }

简单的集成测试:

// test_threadpool.cpp #include "ThreadPool.h" #include <iostream> #include <chrono> int main() { ThreadPool pool(4); std::vector<std::future<int>> futures; // 测试有返回值任务 for (int i = 0; i < 8; ++i) { futures.push_back(pool.submit([i]() -> int { std::this_thread::sleep_for(std::chrono::milliseconds(100)); return i * i; })); } // 测试无返回值任务 for (int i = 0; i < 4; ++i) { pool.submit([i]() { std::this_thread::sleep_for(std::chrono::milliseconds(50)); std::cout << "Hello from task " << i << std::endl; }); } // 获取结果 for (auto& fut : futures) { std::cout << "Result: " << fut.get() << std::endl; } // 测试优雅关闭 pool.shutdown(); std::cout << "All tasks completed, pool shut down." << std::endl; // 尝试在关闭后提交任务(应安全处理或抛出异常,取决于设计) // pool.submit([](){ std::cout << "This should not run" << std::endl; }); return 0; }

通过这个从简到繁、逐步深入的过程,我们不仅实现了一个可用的C++线程池,更关键的是,我们剖析了其设计中的每一个决策点、潜在陷阱和优化方向。下次当你再被问到线程池相关的问题时,你脑海中浮现的将不再是一个模糊的概念或几个干巴巴的参数,而是一幅清晰的、由代码和逻辑构成的动态图景。这才是动手实现的价值所在。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/15 9:21:55

如何优化Agents-A1-8bit性能:内存管理与推理加速技巧

如何优化Agents-A1-8bit性能&#xff1a;内存管理与推理加速技巧 【免费下载链接】Agents-A1-8bit 项目地址: https://ai.gitcode.com/hf_mirrors/mlx-community/Agents-A1-8bit 想要充分发挥Agents-A1-8bit模型的强大能力&#xff1f;&#x1f60a; 这篇终极指南将为你…

作者头像 李华
网站建设 2026/7/15 9:21:02

ChatCut:基于MCP协议与Codex的AI视频生成工具实战指南

如果你还在为制作产品演示视频而头疼——既要写脚本、又要录屏、配音、剪辑&#xff0c;整个过程耗时耗力&#xff0c;那么ChatCut的出现可能会改变你的工作流。这个最近在技术圈引起关注的AI剪辑工具&#xff0c;最大的特点是能够通过与Codex对话的方式&#xff0c;快速生成带…

作者头像 李华
网站建设 2026/7/15 9:20:56

Nodejs 第九章(模块化源码探秘)

1. Node.js模块化基础概念 模块化是现代编程语言中不可或缺的特性&#xff0c;它让开发者能够将复杂的系统拆分为多个独立、可复用的代码单元。在Node.js中&#xff0c;模块化系统经历了从CommonJS到ES Modules的演进过程&#xff0c;形成了两套并行的规范体系。 我第一次接触…

作者头像 李华
网站建设 2026/7/15 9:20:48

值对象设计模式:从Money库学习不可变对象的实现原理

值对象设计模式&#xff1a;从Money库学习不可变对象的实现原理 【免费下载链接】money Value Object that represents a monetary value (using a currencys smallest unit). 项目地址: https://gitcode.com/gh_mirrors/money3/money 值对象设计模式是面向对象编程中的…

作者头像 李华
网站建设 2026/7/15 9:19:32

Pyecharts地图实战篇 - 从数据准备到交互式热力图生成的完整指南

1. Pyecharts地图可视化入门指南第一次接触Pyecharts地图可视化时&#xff0c;我被它的简单高效震惊了。作为一个常年和Excel打交道的分析师&#xff0c;以前要做一个省级销售热力图&#xff0c;至少得折腾半天。现在用Pyecharts&#xff0c;十几行代码就能生成交互式地图&…

作者头像 李华
网站建设 2026/7/15 9:19:26

CANN ops-nn Tanh梯度算子

aclnnTanhGrad 【免费下载链接】ops-nn 本项目是CANN提供的神经网络类计算算子库&#xff0c;实现网络在NPU上加速计算。 项目地址: https://gitcode.com/cann/ops-nn 产品支持情况 产品是否支持Atlas A2 训练系列产品/Atlas 800I A2 推理产品√ 功能说明 算子功能&a…

作者头像 李华