C++线程池实战:从原理到实现,提升多线程编程效率
2026/7/22 4:46:05 网站建设 项目流程

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

在C++的世界里,尤其是当你从单线程的舒适区踏入多线程的复杂领域时,一个绕不开的痛点就是线程的创建与销毁。想象一下,你正在开发一个高并发的网络服务器,或者一个需要处理大量独立计算任务的数据分析程序。如果每次来一个请求或一个任务,你都去std::thread一把,任务结束后再joindetach,会发生什么?

首先,创建线程本身就是一个开销不小的系统调用,涉及内核资源分配、栈空间开辟等。频繁创建销毁,CPU时间会大量浪费在这些“管理”工作上,而不是真正执行你的业务逻辑。其次,操作系统对线程总数是有限制的,无节制地创建线程最终会导致资源耗尽,程序崩溃。更棘手的是,线程调度带来的上下文切换开销,在大量线程争抢CPU时,会成为性能的隐形杀手。

这时,线程池(Thread Pool)就像一个经验丰富的管家,它预先创建好一批“工人”(线程),并让他们待命。当有“工作”(任务)到来时,管家从任务队列里取出一个,分配给一个空闲的工人去执行。工人干完活后,不会解散回家,而是继续等待下一个任务。这个模式完美解决了上述问题:复用线程,避免频繁创建销毁的开销;控制并发线程数量,防止系统过载;将任务提交与执行解耦,提高响应速度

用C++实现一个线程池,不仅是学习多线程编程的绝佳练手项目,更是深入理解生产者-消费者模型、同步原语(如互斥锁、条件变量)、RAII资源管理等核心概念的实战机会。市面上有很多优秀的库(如Intel TBB、微软的PPL),但自己动手实现一个,能让你彻底掌控其内部机理,在面试中面对“线程池七大参数”之类的问题时,也能对答如流,知其然更知其所以然。本文将带你从零开始,构建一个工业级强度的C++线程池,并详解其使用中的每一个细节。

2. 线程池的整体设计与核心思路拆解

一个健壮的线程池,其核心架构可以抽象为三个关键组件:任务队列、工作者线程组、以及管理这些组件同步的机制。我们的设计目标是:线程安全、高效调度、易于使用、能够优雅关闭。

2.1 核心组件与工作流程

1. 任务队列(Task Queue)这是整个线程池的中枢神经系统,一个生产者-消费者模型的典型应用。主线程(或其他任何线程)作为生产者,向队列中提交任务(通常是一个可调用对象,如函数、lambda表达式、std::function)。线程池内的工作者线程作为消费者,从队列中取出任务并执行。这个队列必须是线程安全的,允许多个生产者同时提交,多个消费者同时获取。

2. 工作者线程组(Worker Threads)在池子初始化时,我们就创建固定数量(或根据策略动态调整)的线程。这些线程的生命周期与池子相同。它们的主体逻辑是一个循环:尝试从任务队列中获取任务 -> 获取成功则执行 -> 执行完毕继续尝试获取。如果队列为空,线程应该被阻塞,进入等待状态,而不是空转消耗CPU。

3. 同步与通信机制这主要依靠互斥锁(std::mutex)和条件变量(std::condition_variable)来实现。

  • 互斥锁:保护任务队列,确保同一时间只有一个线程(生产者或消费者)在修改队列状态(入队或出队)。
  • 条件变量:用于线程间的等待和通知。当队列为空时,工作者线程在条件变量上等待;当有新任务入队时,生产者通知(notify_onenotify_all)等待的线程。同样,在关闭池子时,也需要条件变量来通知所有线程退出循环。

工作流程简述

  1. 初始化:创建N个工作者线程,它们启动后立即尝试从空队列获取任务,从而阻塞在条件变量上。
  2. 提交任务:用户调用submitenqueue函数,将任务包装后放入任务队列,然后通知一个(或所有)等待的工作者线程。
  3. 执行任务:被通知的工作者线程被唤醒,获取互斥锁,从队列中取出任务,释放锁,然后执行该任务。
  4. 循环与关闭:线程执行完任务后,再次回到“尝试获取任务”的步骤。当收到关闭信号时,所有线程完成当前任务后退出循环,主线程等待所有工作者线程join

2.2 设计决策与权衡

1. 固定大小 vs 动态伸缩我们选择实现一个固定大小的线程池。这是最简单、最稳定、也是最常见的模式。线程数量在构造时指定,生命周期内不变。动态线程池(如根据队列长度动态增减线程)虽然更灵活,但引入了更复杂的线程创建/销毁逻辑和伸缩策略,容易引发抖动,对于大多数场景,固定大小的池子经过合理配置(如设置为CPU核心数或略多)已经足够高效。Java的ThreadPoolExecutor核心参数之一就是核心线程数,其设计思想也值得借鉴。

2. 任务队列的实现选择我们使用std::queue<std::function<void()>>作为底层容器。std::function可以包装任何可调用对象,提供了极大的灵活性。队列本身用std::queue,操作简单。更高级的实现可以考虑使用std::dequestd::priority_queue来支持任务优先级,但为了核心逻辑清晰,我们先从基础做起。

3. 结果获取:Future/Promise模式这是提升易用性的关键。我们不希望提交任务后完全无法控制。我们将实现submit函数,让它返回一个std::future。这样,提交方可以在未来某个时刻通过这个future来获取任务的返回值或检查异常。这需要在提交时,将任务与一个std::promise打包,任务执行完毕后将结果或异常设置到promise中。

4. 优雅关闭策略这是线程池的难点之一。我们设计一个“软关闭”流程:设置一个停止标志(std::atomic<bool>),当调用shutdown时,标志置位,并通知所有等待线程。工作者线程在每次循环检查这个标志,如果为真且任务队列为空,则退出循环。shutdown函数会等待(join)所有工作者线程结束。我们还可以提供一个shutdown_now选项,立即停止并清空队列,但这可能导致任务丢失,需谨慎使用。

3. 核心细节解析与实现要点

3.1 任务封装与类型擦除

任务队列里要存放什么?我们需要一个统一的类型来代表“一段可以异步执行的代码”。std::function<void()>完美胜任。它通过类型擦除技术,可以包装函数指针、成员函数指针、lambda表达式、bind表达式等任何签名兼容的可调用对象。

// 一个简单的任务类型 using Task = std::function<void()>; std::queue<Task> tasks_;

但是,为了支持返回值和异常传递,我们需要更精巧的包装。我们将任务包装成一个返回voidstd::packaged_task,并将其结果与一个std::future绑定。

// 一个辅助函数,用于将任意可调用对象包装成基础Task 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本身是可调用的,调用它会执行f,并将结果存储到内部的共享状态中 auto task = 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> res = task->get_future(); { std::unique_lock<std::mutex> lock(queue_mutex_); if(stop_.load()) { throw std::runtime_error("submit on a stopped ThreadPool"); } // 将任务包装成一个void()的lambda,放入队列 // 这个lambda在执行时,会调用(*task)(),即执行真正的函数 tasks_.emplace([task]() { (*task)(); }); } // 通知一个等待的线程 condition_.notify_one(); return res; }

注意:这里使用了std::make_shared来管理packaged_task的生命周期。因为std::packaged_task是不可拷贝的,但我们需要将其捕获到lambda中,而lambda可能被拷贝(当放入std::function时)。通过智能指针共享,我们安全地转移了所有权。这是实现中的关键技巧。

3.2 线程安全队列的实现细节

我们的任务队列需要支持多线程并发访问,必须保证:

  1. 互斥访问:入队(push)和出队(pop)操作不能同时进行。
  2. 条件同步:消费者在队列空时等待,生产者在入队后通知。
// 简化的线程安全队列核心逻辑(在ThreadPool类内部) std::mutex queue_mutex_; std::condition_variable condition_; std::queue<Task> tasks_; // 工作者线程的主循环函数 void worker() { while(true) { Task task; { // 1. 获取互斥锁 std::unique_lock<std::mutex> lock(this->queue_mutex_); // 2. 等待条件:条件变量被唤醒,并且(队列非空 或 线程池已停止) this->condition_.wait(lock, [this]() { return this->stop_.load() || !this->tasks_.empty(); } ); // 3. 检查是否因停止且队列空而退出 if(this->stop_.load() && this->tasks_.empty()) { return; } // 4. 出队任务 task = std::move(this->tasks_.front()); this->tasks_.pop(); } // 5. 锁在作用域结束时自动释放 // 6. 执行任务(在锁外执行,避免长时间持有锁阻塞其他线程) task(); } }

实操心得condition_variable::wait的谓词(第二个参数lambda)至关重要。它防止了虚假唤醒(spurious wakeup)——即线程可能在没有被notify的情况下被操作系统唤醒。谓词检查了真实的等待条件(!tasks_.empty()),只有条件满足时,wait才会返回。同时,我们将停止标志stop_也纳入谓词,这样在关闭时能快速唤醒所有线程进行检查。

3.3 优雅关闭的完整逻辑

关闭线程池需要协调所有线程,确保没有任务被遗漏,也没有线程被永远阻塞。

// 在ThreadPool类中 std::atomic<bool> stop_{false}; std::vector<std::thread> workers_; void shutdown() { { std::unique_lock<std::mutex> lock(queue_mutex_); stop_.store(true); } // 修改stop_后立即释放锁 condition_.notify_all(); // 通知所有等待的线程 // 等待所有线程执行完毕 for(std::thread &worker: workers_) { if(worker.joinable()) { worker.join(); } } workers_.clear(); } // 析构函数中自动调用shutdown ~ThreadPool() { shutdown(); }

注意事项:一定要在修改stop_标志并notify_all之后,再进行join。顺序反过来会导致死锁:如果先join,主线程会阻塞等待工作者线程结束,而工作者线程可能正在条件变量上等待,永远无法被唤醒。另外,在submit函数中,一旦检测到stop_为真,应立即抛出异常,防止向已停止的池子提交新任务。

4. 完整实现与核心代码剖析

下面我们将上述设计整合成一个完整的、可复用的ThreadPool类。为了清晰,我们分块解析。

4.1 类定义与成员变量

#include <vector> #include <queue> #include <memory> #include <thread> #include <mutex> #include <condition_variable> #include <future> #include <functional> #include <stdexcept> #include <atomic> class ThreadPool { public: // 构造函数,显式创建指定数量的线程 explicit ThreadPool(size_t thread_count = std::thread::hardware_concurrency()); // 禁止拷贝和赋值 ThreadPool(const ThreadPool&) = delete; ThreadPool& operator=(const ThreadPool&) = delete; // 析构函数,自动关闭 ~ThreadPool(); // 核心接口:提交一个任务,返回一个future template<class F, class... Args> auto submit(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>; // 关闭线程池(等待所有任务完成) void shutdown(); private: // 工作者线程需要访问池的私有成员,故需要将worker函数设为私有成员 void worker(); // 成员变量 std::vector<std::thread> workers_; // 工作者线程容器 std::queue<std::function<void()>> tasks_; // 任务队列 // 同步原语 std::mutex queue_mutex_; // 保护任务队列的互斥锁 std::condition_variable condition_; // 任务队列非空的条件变量 // 停止标志 std::atomic<bool> stop_{false}; };

关键点

  • std::thread::hardware_concurrency()是一个很有用的函数,它返回当前硬件支持的并发线程数(通常是CPU核心数),作为默认线程数是一个合理的起点。
  • 使用std::result_of(C++17前)或std::invoke_result(C++17后)来推导提交函数的返回类型,使submit接口更通用。
  • 将拷贝构造和赋值运算符设为delete,因为线程池管理着资源(线程),拷贝语义不明确且危险。

4.2 构造函数与工作者线程启动

ThreadPool::ThreadPool(size_t thread_count) { if(thread_count == 0) { thread_count = 1; // 至少一个线程 } workers_.reserve(thread_count); for(size_t i = 0; i < thread_count; ++i) { // 创建线程,并立即执行worker成员函数 workers_.emplace_back([this] { this->worker(); }); } }

关键点:在构造函数中启动所有线程。每个线程执行的都是同一个worker()成员函数。这里使用lambda捕获this指针来访问当前对象的成员。reserve预先分配内存,避免vectoremplace_back时多次扩容。

4.3submit成员函数模板的实现

这是线程池最精妙的部分,它处理了任意类型任务和结果返回。

template<class F, class... Args> auto ThreadPool::submit(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> { using return_type = typename std::result_of<F(Args...)>::type; // 创建一个指向packaged_task的shared_ptr // packaged_task<return_type()> 表示一个封装了返回return_type的无参数函数的任务 auto task_ptr = std::make_shared<std::packaged_task<return_type()>>( // 使用bind和完美转发将函数f和参数args绑定成一个无参可调用对象 std::bind(std::forward<F>(f), std::forward<Args>(args)...) ); // 获取与该packaged_task关联的future std::future<return_type> res = task_ptr->get_future(); { // 锁住队列,准备入队 std::unique_lock<std::mutex> lock(queue_mutex_); // 如果线程池已停止,拒绝提交新任务 if(stop_.load()) { throw std::runtime_error("submit called on a stopped ThreadPool"); } // 将实际执行逻辑包装成一个void()的lambda,放入任务队列 // 这个lambda捕获task_ptr,执行时调用(*task_ptr)() tasks_.emplace([task_ptr]() { (*task_ptr)(); // 执行真正的函数,结果会自动存入packaged_task的共享状态 }); } // 锁的作用域结束,自动释放 // 通知一个正在等待的工作者线程 condition_.notify_one(); // 将future返回给调用者 return res; }

深度解析

  1. std::bind与完美转发std::bind将用户提供的函数f和参数args...绑定成一个新的可调用对象。std::forward是完美转发,保持参数原有的左值/右值引用属性,避免不必要的拷贝。
  2. std::packaged_task:这是一个高级抽象,它将一个可调用对象与其结果存储(一个std::future)关联起来。调用(*task_ptr)()不仅执行了函数,还会自动将返回值(或异常)设置到内部的共享状态中。
  3. Lambda捕获与生命周期:我们捕获的是task_ptr(智能指针),而不是packaged_task对象本身。这确保了无论packaged_task被移动到何处(比如在队列里),其生命周期都由shared_ptr管理,直到任务被执行完毕。这是实现任务安全传递的核心。
  4. 异常安全:如果任务执行中抛出异常,异常会被packaged_task捕获并存储,随后通过future::get()重新抛出给调用get()的线程。这保证了异常不会在线程池内部被吞没。

4.4 工作者线程函数worker()

void ThreadPool::worker() { // 线程主循环 while(true) { std::function<void()> task; // 用于存放从队列取出的任务 { // 步骤1:获取队列锁 std::unique_lock<std::mutex> lock(queue_mutex_); // 步骤2:等待条件成立。条件:池子未停止且队列为空时,才等待。 // 这个lambda是wait的“谓词”,返回false时,线程才会进入等待阻塞。 condition_.wait(lock, [this]() -> bool { return this->stop_.load() || !this->tasks_.empty(); }); // 步骤3:检查退出条件 // 如果池子已停止,并且队列已空,则此线程可以结束了 if(this->stop_.load() && this->tasks_.empty()) { return; // 退出循环,线程函数结束 } // 步骤4:从队列中取出任务 // 走到这里,意味着要么有任务(!tasks_.empty()),要么是虚假唤醒但条件仍满足。 // 使用move语义转移任务所有权,避免拷贝开销。 task = std::move(this->tasks_.front()); this->tasks_.pop(); } // 步骤5:锁的作用域结束,自动释放锁。**关键:在锁外执行任务!** // 步骤6:执行取出的任务 task(); } }

为什么要在锁外执行任务?这是性能优化的关键点。任务task()的执行时间可能很长,可能是I/O操作、复杂计算等。如果我们在持有queue_mutex_锁的情况下执行task(),那么在这段时间内,其他所有线程(包括想提交任务的生产者和其他想取任务的工作者)都会被阻塞,导致并发度急剧下降,线程池几乎退化为串行。因此,我们遵循“锁粒度最小化”原则:锁只保护共享数据(队列)的访问,一旦数据取出,立即释放锁,让其他线程可以继续操作队列。

4.5 关闭与析构

void ThreadPool::shutdown() { { std::unique_lock<std::mutex> lock(queue_mutex_); stop_.store(true); // 原子地设置停止标志 } // 这里释放锁很重要,确保通知前锁已释放 condition_.notify_all(); // 唤醒所有正在wait的工作者线程 // 等待所有线程自然结束(执行完worker函数中的return) for(std::thread &worker : workers_) { if(worker.joinable()) { worker.join(); } } workers_.clear(); // 清空线程容器(可选) } ThreadPool::~ThreadPool() { shutdown(); // 析构时自动关闭,确保资源释放 }

关键点shutdown函数是幂等的,多次调用是安全的。stop_.store(true)condition_.notify_all()的顺序可以互换,但通常先设置标志再通知逻辑更清晰。一定要在notify_all之后再进行join,如前所述。

5. 使用详解与高级技巧

有了完整的ThreadPool类,我们来看看如何在实际项目中使用它,并探讨一些高级用法和配置。

5.1 基础用法示例

#include "ThreadPool.h" #include <iostream> #include <chrono> int computeSquare(int x) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟耗时操作 return x * x; } int main() { // 1. 创建一个默认线程数(CPU核心数)的线程池 ThreadPool pool; // 2. 提交一批任务,并收集future std::vector<std::future<int>> futures; for(int i = 1; i <= 10; ++i) { // 使用submit提交任务,支持函数、lambda、成员函数等 futures.emplace_back(pool.submit(computeSquare, i)); // 也可以提交lambda // futures.emplace_back(pool.submit([](int n){ return n*n; }, i)); } // 3. 主线程可以继续做其他工作... std::cout << "Tasks submitted, main thread is free now.\n"; // 4. 在需要的时候,通过future获取结果 for(auto &future : futures) { // future.get() 会阻塞,直到对应的任务完成并返回结果 // 如果任务抛出了异常,get()会重新抛出该异常 int result = future.get(); std::cout << "Result: " << result << std::endl; } // 5. 线程池会在main函数结束时,随着pool对象析构而自动关闭 // 也可以手动调用 pool.shutdown(); return 0; }

5.2 处理任务异常

线程池的一个巨大优势是能将子线程的异常安全地传递回主线程。

void mightThrow() { if(std::rand() % 2) { throw std::runtime_error("Something bad happened in a task!"); } std::cout << "Task completed successfully.\n"; } int main() { ThreadPool pool(4); auto future = pool.submit(mightThrow); try { future.get(); // 如果任务抛出了异常,get()会在这里重新抛出 std::cout << "Got result (or void).\n"; } catch (const std::exception& e) { std::cerr << "Caught exception from thread pool task: " << e.what() << std::endl; } return 0; }

5.3 实现任务优先级

基础版本是FIFO队列。如果需要优先级,可以将std::queue替换为std::priority_queue,并定义任务优先级。通常需要定义一个包含std::function和优先级数值的结构体,并重载比较运算符。

struct PrioritizedTask { int priority; std::function<void()> task; // 优先级高的先出队(注意:priority_queue默认是最大堆) bool operator<(const PrioritizedTask& other) const { return priority < other.priority; // 数字小的优先级低 } }; // 在ThreadPool中 std::priority_queue<PrioritizedTask> tasks_; // 提交任务时需要指定优先级 template<class F, class... Args> auto submit_with_priority(int priority, F&& f, Args&&... args) -> ... { // ... 类似submit,但将打包好的lambda和priority一起放入PrioritizedTask tasks_.emplace(priority, [task_ptr](){ (*task_ptr)(); }); // ... }

注意事项:引入优先级会增加队列操作的复杂度(从O(1)到O(log n)),并且需要仔细设计优先级反转等场景的处理。对于大多数均匀负载的场景,FIFO队列简单高效。

5.4 动态调整线程数量(进阶)

固定大小线程池简单可靠,但某些场景可能需要弹性。一个常见的动态策略是:维护一个“核心线程数”和“最大线程数”。当队列长度超过某个阈值,且当前线程数小于最大线程数时,创建新线程;当线程空闲时间超过一定时长,且大于核心线程数时,回收该线程。实现这个逻辑更为复杂,需要管理线程的空闲状态和生命周期,并引入更多的条件变量和超时等待(wait_for/wait_until)。

6. 常见问题、性能调优与避坑指南

在实际使用自研线程池时,你会遇到一些典型问题和优化点。

6.1 死锁与竞态条件排查

  • 问题1:shutdown死锁

    • 现象:程序在调用shutdown或析构时挂起。
    • 原因:最常见的原因是join顺序不当,或者在worker线程中持有了某个外部锁,而该锁在shutdown时被主线程获取,导致循环等待。
    • 排查:确保shutdown中先notify_all()join()。检查任务函数内部是否使用了全局锁或静态锁,并确保其不会与池的管理锁产生死锁。
  • 问题2:任务提交后永不执行

    • 现象future.get()一直阻塞。
    • 原因
      1. 线程池在任务提交前就已停止(stop_为true),任务被拒绝(我们的实现会抛异常)。
      2. 所有工作者线程都因任务中的未捕获异常而退出(在我们的实现中,异常被packaged_task捕获并存储到future,不会导致线程退出)。
      3. 更隐蔽的原因:任务本身阻塞在了某个I/O或同步操作上,且该操作的条件永远无法满足。
    • 排查:使用调试器查看所有工作者线程的状态。检查stop_标志。为任务添加超时机制(如使用future.wait_for)。

6.2 性能调优要点

  1. 线程数量设置:这是最重要的参数。“CPU密集型”任务(如计算圆周率、图像处理),线程数建议设置为CPU核心数CPU核心数+1,过多会导致频繁的上下文切换,降低性能。“I/O密集型”任务(如网络请求、文件读写),线程数可以设置得多一些,比如2 * CPU核心数或更多,因为线程在等待I/O时会让出CPU,让其他线程执行。最佳值需要通过压力测试来确定。
  2. 任务队列长度:我们的实现使用了无界队列。在生产环境中,无界队列可能导致内存耗尽。一个改进是使用有界队列(如固定大小的环形缓冲区),当队列满时,提交任务可以采取不同的策略:阻塞提交者、直接拒绝并抛出异常、或者调用者自己执行任务(Caller-Runs Policy)。这模仿了Java线程池的拒绝策略。
  3. 避免任务粒度太小:如果每个任务都极其简单(例如只是对一个整数加1),那么线程间通信(锁竞争、条件变量通知)的开销可能会超过任务本身的计算开销。这种情况下,应考虑任务批处理,将多个小任务合并成一个稍大的任务提交。
  4. 使用std::asyncstd::async是C++11标准库提供的异步任务接口,它可能使用线程池(取决于实现),但行为不完全由你控制。对于需要精细控制并发度、任务队列和生命周期的场景,自定义线程池是更优选择。

6.3 线程池的“坑”与应对策略

  • 线程局部存储(TLS)问题:如果你的任务使用了thread_local变量,需要注意,线程池中的线程是复用的。一个线程执行完任务A后,它的TLS状态会保留,当它执行任务B时,B可能会读到A留下的“脏数据”。务必在每个任务的开始处,初始化或清理所需的TLS状态。
  • 阻塞性任务:如果一个任务长时间阻塞(例如,等待一个永远不会到来的网络包),它会独占一个工作者线程。如果这样的任务多了,线程池的有效并发度就会下降。考虑为这类操作设置超时,或者使用专门的异步I/O库。
  • 递归提交任务:任务A向同一个线程池提交了任务B,而任务B又提交了任务C……这在某些算法中(如并行快速排序)是合理的,但要注意死锁风险。如果线程池大小有限,且所有线程都在等待递归提交的子任务完成,而子任务又在队列中排队等待空闲线程,就会发生死锁。这种情况下,可能需要使用“工作窃取”(Work-Stealing)算法的高级线程池,或者确保递归深度和任务粒度是可控的。

6.4 一个简单的性能测试对比

我们可以写个小程序,对比使用线程池和直接创建线程执行大量短任务的耗时。

void shortTask(int id) { // 模拟一个非常短的计算 volatile int sum = 0; // volatile防止被优化掉 for(int i = 0; i < 1000; ++i) { sum += i; } } int main() { const int TASK_COUNT = 10000; // 测试1:直接创建线程(极端情况,每个任务一个线程) auto start1 = std::chrono::high_resolution_clock::now(); { std::vector<std::thread> threads; threads.reserve(TASK_COUNT); for(int i = 0; i < TASK_COUNT; ++i) { threads.emplace_back(shortTask, i); } for(auto& t : threads) { t.join(); } } auto end1 = std::chrono::high_resolution_clock::now(); // 测试2:使用线程池(4个线程) auto start2 = std::chrono::high_resolution_clock::now(); { ThreadPool pool(4); std::vector<std::future<void>> futures; futures.reserve(TASK_COUNT); for(int i = 0; i < TASK_COUNT; ++i) { futures.emplace_back(pool.submit(shortTask, i)); } // 通过get等待所有任务完成 for(auto& f : futures) { f.get(); } } // pool析构,自动shutdown auto end2 = std::chrono::high_resolution_clock::now(); auto duration1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1); auto duration2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2); std::cout << "Direct thread creation: " << duration1.count() << " ms\n"; std::cout << "ThreadPool (4 workers): " << duration2.count() << " ms\n"; return 0; }

在我的测试环境(8核CPU)上,运行10000个极短任务,直接创建线程的方式可能耗时数秒甚至因资源限制失败,而4线程的线程池通常能在几十毫秒内完成,优势巨大。这直观地展示了线程复用带来的性能红利。

通过从原理到实现,再到使用和优化的全程剖析,这个基于C++11/14标准库的线程池不仅是一个可用的工具,更是一个理解现代C++并发编程的绝佳样本。你可以在此基础上,根据实际需求添加更多功能,如任务取消、进度汇报、监控统计等,使其更加强大和贴合你的项目。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询