news 2026/8/13 22:10:52

现代 C++ 异步编程:从零实现一个高性能 ThreadPool (C++20 深度实践)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
现代 C++ 异步编程:从零实现一个高性能 ThreadPool (C++20 深度实践)

在高性能 C++ 开发中,线程池是绕不开的核心基础设施。随着 C++20 标准的普及,我们能够以更简洁、更安全的方式实现一个生产级的线程池。本文将带你深度剖析一个基于std::jthread的线程池实现,并探讨其背后的架构思考与内存管理机制。


1. 核心代码实现 (C++20 版)

这个线程池利用了 C++20 的 RAII 线程管理特性,彻底告别了手动join的时代。

#pragmaonce#include<condition_variable>#include<functional>#include<future>#include<memory>#include<mutex>#include<queue>#include<stdexcept>#include<thread>#include<type_traits>#include<utility>#include<vector>classThreadPool{public:explicitThreadPool(std::size_t threads){if(threads==0){throwstd::invalid_argument("ThreadPool requires at least one worker");}workers.reserve(threads);for(std::size_t i=0;i<threads;++i){// std::jthread 会自动管理线程生命周期。//// jthread 析构时会:// 1. request_stop()// 2. join()//// stop_token 是 worker 获取停止请求的方式。workers.emplace_back([this](std::stop_token st){// 不要写://// while (!st.stop_requested())//// 因为我们的策略是://// 收到 stop// ↓// 不再等待新任务// ↓// 把已经在队列中的任务执行完// ↓// worker 退出//// 所以这里使用无限循环,由下面的逻辑决定什么时候退出。for(;;){std::function<void()>task;{std::unique_lock<std::mutex>lock(queue_mutex);// 等待两个条件之一://// 1. tasks 非空// 2. stop_token 收到 stop 请求//// condition_variable_any 支持 stop_token,// 因此 request_stop() 可以唤醒这里的 worker。//// wait() 返回://// true -> predicate 满足,即 tasks 非空// false -> stop_token 收到了 stop 请求boolok=condition.wait(lock,st,[this]{return!tasks.empty();});// stop 请求,并且任务队列已经为空://// stop == true// tasks.empty() == true//// 说明没有任何任务需要继续处理,可以退出。if(!ok&&tasks.empty()){return;}// 到这里有两种可能://// 1. 正常收到任务// 2. 收到 stop,但队列里还有任务//// 两种情况都需要取出任务执行。task=std::move(tasks.front());tasks.pop();}// 非常重要://// 不要持有 queue_mutex 执行任务。//// 如果任务执行时间很长,而 mutex 一直被占用,// 其他 worker 就无法从任务队列取任务。//// 所以://// 加锁// ↓// 取任务// ↓// 解锁// ↓// 执行任务task();}});}}~ThreadPool()=default;template<classF,class...Args>autoenqueue(F&&f,Args&&...args)->std::future<std::invoke_result_t<F,Args...>>{// 获取任务函数的返回类型。//// 例如://// int foo(int);//// enqueue(foo, 10)//// return_type == intusingreturn_type=std::invoke_result_t<F,Args...>;// packaged_task 用于://// worker 执行任务// ↓// 保存返回值// ↓// future 获取返回值//// packaged_task 本身是 move-only。//// 所以这里使用 shared_ptr 管理它的生命周期。autotask=std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f),std::forward<Args>(args)...));// future 是调用者获取任务执行结果的方式。std::future<return_type>result=task->get_future();{std::unique_lock<std::mutex>lock(queue_mutex);// tasks 的统一类型是://// std::function<void()>//// 不管用户提交的是://// int foo()// void bar()// std::string baz()//// 最终都包装成://// void()//// worker 不需要知道任务的真实类型。tasks.emplace([task]{(*task)();});}// mutex 已经释放之后再通知 worker。//// notify_one() 唤醒一个正在等待任务的 worker。condition.notify_one();returnresult;}private:/* * 成员变量的声明顺序非常重要。 * * C++ 对成员变量的析构顺序是: * * 声明顺序的逆序 * * 所以这里实际上会按照: * * workers * ↓ * tasks * ↓ * condition * ↓ * queue_mutex * * 的顺序析构。 * * workers 必须最后声明。 * * 原因: * * worker 线程里面会访问: * * queue_mutex * condition * tasks * * 因此必须保证 worker 全部结束之后, * 这些对象才开始析构。 */// 保护 tasks。std::mutex queue_mutex;// condition_variable_any 支持 stop_token。std::condition_variable_any condition;// 等待执行的任务。std::queue<std::function<void()>>tasks;// 必须最后声明。//// 析构时 workers 最先被析构://// ~jthread()// ↓// request_stop()// ↓// worker 被唤醒// ↓// worker 退出// ↓// join()//// 等所有 worker 结束以后,// tasks / condition / queue_mutex 才会析构。std::vector<std::jthread>workers;};

2. 深度剖析:为什么不直接存packaged_task

这是初学者常有的疑问:既然std::packaged_task已经封装了任务,为什么队列里存的是std::function<void()>

类型擦除 (Type Erasure)

std::packaged_task<R(Args...)>是一个强类型模板,它的类型包含了返回值R

  • 如果任务 A 返回int,任务 B 返回string,它们的类型是不同的。
  • std::queue只能存储相同类型的对象。

解决方案:我们利用std::function<void()>作为“通用信封”。在enqueue内部,我们写了一个 Lambda 表达式:[task](){ (*task)(); }。无论原始任务返回什么,这个 Lambda 永远是void()类型。这在设计模式中被称为类型擦除,它抹平了不同任务之间的差异。


3. 内存管理:智能指针的“接力”

在异步环境下,最怕的是“任务还没跑,对象先没了”。本实现通过std::shared_ptr和 Lambda 捕获完美解决了生存周期问题。

  1. 诞生:在enqueue里通过make_shared创建任务。此时计数为 1。
  2. 接力:Lambda 表达式通过按值捕获[task]。这意味着shared_ptr被拷贝了一份存进了 Lambda 闭包中。此时计数为 2。
  3. 入队:Lambda 被存入std::function并进入队列。即使enqueue函数返回,闭包依然拉着shared_ptr
  4. 销毁:工作线程取出并执行完任务后,std::function被销毁,引用计数降为 0,任务对象内存自动释放。

这种“引用计数+闭包捕获”的机制,保证了任务只要在队列中,内存就绝对安全。


4. C++20 的技术红利

std::jthread 与 RAII

传统的std::thread在析构时如果不joindetach会导致进程异常退出。std::jthread引入了 RAII(资源获取即初始化)机制,它在析构时会自动发出停止请求并等待线程结束,使得线程池的析构函数极其简洁。

协作式中断 (stop_token)

通过std::stop_token,我们不再需要手动维护一个bool stop标志。
condition.wait(lock, st, pred)更是神来之笔:当jthread准备停止时,它会自动唤醒所有阻塞在条件变量上的线程。这比以前手动notify_all()要健壮得多。


5. 如何使用

intmain(){ThreadPoolpool(4);// 提交带返回值的异步任务autofuture=pool.enqueue([](intx){returnx*x;},10);// 获取结果 (会阻塞直到任务完成)std::cout<<"Result: "<<future.get()<<std::endl;return0;}

6. 总结

实现一个线程池不难,但实现一个既类型安全内存无忧的线程池需要对 C++ 的底层机制有深刻理解。

  • std::function<void()>解决了异质任务的存储问题。
  • std::shared_ptr解决了异步任务的生命周期问题。
  • std::jthread解决了线程资源的回收问题。

这个短小精悍的实现,正是 C++ 现代化的魅力所在。


版权声明:本文采用 CC BY-SA 4.0 协议,转载请注明出处。

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

开发者技术搜索安全指南:从官方文档到代码验证的全流程实践

最近在技术社区看到不少关于“最可怕的搜索引擎”的讨论&#xff0c;很多开发者&#xff0c;尤其是刚入门的新手&#xff0c;在寻找技术解决方案时&#xff0c;可能会误入一些存在安全风险、隐私泄露或提供恶意代码的网站。本文并非要讨论某个具体的搜索引擎&#xff0c;而是想…

作者头像 李华
网站建设 2026/8/13 22:04:52

软考高项新考点:数字化转型、数据要素、元宇宙相关概念体系梳理

摘要本文系统梳理了软考高级信息系统项目管理师&#xff08;高项&#xff09;考试中新增的三个重要考点&#xff1a;数字化转型、数据要素和元宇宙。文章从核心概念、关键特征、技术体系、应用场景及对项目管理的影响等多个维度进行阐述&#xff0c;旨在帮助考生构建清晰的知识…

作者头像 李华
网站建设 2026/8/13 21:59:11

React Native与OpenHarmony动态标题栏开发实践

1. 为什么需要动态标题&#xff1f; 在移动应用开发中&#xff0c;标题栏&#xff08;TitleBar&#xff09;是与用户交互的重要界面元素。传统开发模式下&#xff0c;我们通常在页面组件挂载时通过 setOptions 静态设置标题&#xff0c;这种方式存在几个明显痛点&#xff1a;…

作者头像 李华
网站建设 2026/8/13 21:58:33

嵌入式面试(十二)——SRAM vs DRAM应用场景深度解析

一、引言&#xff1a;为什么需要了解SRAM与DRAM&#xff1f;在嵌入式系统开发与面试中&#xff0c;SRAM&#xff08;静态随机存取存储器&#xff09;与DRAM&#xff08;动态随机存取存储器&#xff09;是两类核心的半导体存储器。理解它们的原理、差异和适用场景&#xff0c;不…

作者头像 李华
网站建设 2026/8/13 21:56:23

真题(来自一本通)

2110&#xff1a;【例5.1】素数环时间限制: 1000 ms 内存限制: 65536 KB 提交数:19024 通过数: 7285【题目描述】输入正整数nn&#xff0c;把整数11,22,…,nn 组成一个环&#xff0c;使得相邻两个整数之和均为素数。【输入】输入正整数nn。【输出】输出任意一个满足…

作者头像 李华