【C++ 标准项目】发布订阅消息队列 (篇五):C++11 异步编程,手写线程池,async/promise/packaged_task/future 实战
C11 异步操作实现线程池1.std::futurea.介绍链接如下cplusplus.com/reference/future/future/std::future 是 C11 标准库中的一个模板类它表示一个异步操作的结果。当我们在多线程编程中使用异步任务时std::future 可以帮助我们在需要的时候获取任务的执行结果。std::future 的一个重要特性是能够阻塞当前线程直到异步操作完成从而确保我们在获取结果时不会遇到未完成的操作。b.应用场景异步任务当我们需要在后台执行一些耗时操作时如网络请求或计算密集型任务等std::future 可以用来表示这些异步任务的结果。通过将任务与主线程分离我们可以实现任务的并行处理从而提高程序的执行效率并发控制在多线程编程中我们可能需要等待某些任务完成后才能继续执行其他操作。通过使用 std::future我们可以实现线程之间的同步确保任务完成后再获取结果并继续执行后续操作结果获取std::future 提供了一种安全的方式来获取异步任务的结果。我们可以使用std::future::get()函数来获取任务的结果此函数会阻塞当前线程直到异步操作完成。这样在调用 get()函数时我们可以确保已经获取到了所需的结果c.用法示例使用 std::async 关联异步任务std::async 是一种将任务与 std::future 关联的简单方法。它创建并运行一个异步任务并返回一个与该任务结果关联的 std::future 对象。默认情况下std::async 是否启动一个新线程或者在等待 future 时任务是否同步运行都取决于你给的 参数。这个参数为 std::launch 类型 std::launch::deferred 表明该函数会被延迟调用直到在 future 上调用 get()或者 wait()才会开始执行任务 std::launch::async 表明函数会在自己创建的线程上运行 std::launch::deferred | std::launch::async 内部通过系统等条件自动选择策略来看代码运行结果使用 std::packaged_task 和 std::future 配合std::packaged_task 就是将任务和 std::future 绑定在一起的模板是一种对任务的封装。我们可以通过 std::packaged_task 对象获取任务相关联的 std::future 对象通过调用 get_future()方法获得。std::packaged_task 的模板参数是函数签名。可以把 std::future 和 std::async 看成是分开的 而 std::packaged_task 则是一个整体。来看代码运行结果使用 std::promise 和 std::future 配合std::promise 提供了一种设置值的方式它可以在设置之后通过相关联的 std::future 对象进行读取。换种说法就是之前说过 std::future 可以读取一个异步函数的返回值了 但是要等待就绪而 std::promise 就提供一种 方式手动让 std::future 就绪来看代码2.c11 线程池实现来看代码运行结果代码解析如下a.整体结构简答说就是push负责交任务工作线程负责取任务执行future负责传结果。b.核心流程push()的四步templatetypename F, typename ...Args auto push(F func, Args ...args) - std::futuredecltype(func(args...)) { // 1. 把函数 参数绑成一个整体并推断返回类型 using return_type decltype(func(args...)); auto tmp_func std::bind(std::forwardF(func), std::forwardArgs(args)...); // 2. 用 packaged_task 包装它并用 shared_ptr 持有 auto task std::make_sharedstd::packaged_taskreturn_type()(tmp_func); std::futurereturn_type fu task-get_future(); // 取出配对凭证 // 3. 构造一个只捕获 shared_ptr 的 lambda塞进队列 { std::unique_lockstd::mutex lock(_mutex); _taskpool.push_back([task](){ (*task)(); }); _cv.notify_one(); // 叫醒一个工作线程 } return fu; // 4. 把凭证交给调用方 }entry()的三步void entry() { while(!_stop) { std::vectorfunctor tmp_taskpool; { std::unique_lockstd::mutex lock(_mutex); // 1. 等任务池非空 或 要停了 _cv.wait(lock, [this](){ return _stop || !_taskpool.empty(); }); // 2. 把整批任务 swap 到本地 tmp_taskpool.swap(_taskpool); } // 3. 【锁已经释放】逐个执行 for (auto task : tmp_taskpool) task(); } }c.四个关键设计1. 为什么必须用shared_ptr包packaged_task这是整段代码最关键的一处很多人写线程池就卡在这里std::vectorfunctor _taskpool; // functor std::functionvoid()std::function要求存进去的可调用对象是可拷贝的。但std::packaged_task是move-only只能移动不能拷贝std::packaged_taskint() t1(...); std::packaged_taskint() t2 t1; // 错误 拷贝构造被删除 _taskpool.push_back(task); // 错误 packaged_task 无法放进 std::function解法用shared_ptr包一层。shared_ptr是可拷贝的lambda 捕获它也只是增加引用计数auto task std::make_sharedstd::packaged_taskreturn_type()(tmp_func); _taskpool.push_back([task](){ (*task)(); }); // 正确 shared_ptr 可拷贝同时它还顺手解决了生命周期问题task被 lambda 捕获 -- 引用计数 1任务执行完lambda 被销毁 -- 引用计数 -1 -- 自动释放不用手动delete也不用担心任务还在队列里对象就没了小结shared_ptr 在这里干了两件事——让 move-only 的对象能被 std::function 持有以及自动管理生命周期。2.为什么要把任务swap到本地再执行{ std::unique_lockstd::mutex lock(_mutex); tmp_taskpool.swap(_taskpool); // - 只做这一件事然后立刻解锁 } for (auto task : tmp_taskpool) task(); // - 在锁【外面】执行如果直接在锁里执行任务会怎样// 反例 { std::unique_lockstd::mutex lock(_mutex); while(!_taskpool.empty()) { _taskpool.front()(); // 执行任务可能耗时很久 _taskpool.pop_front(); } }后果某个任务跑了 5 秒这 5 秒内其他线程一个都 push 不进来锁被占着—— 线程池变成了串行池。swap 的妙处加锁的时间只有交换两个 vector 指针那么短O(1)任务的实际执行完全不持锁。3. 为什么wait必须带谓词_cv.wait(lock, [this](){ return _stop || !_taskpool.empty(); });等价于while (!(_stop || !_taskpool.empty())) { // --- 必须用 while不是 if _cv.wait(lock); }两个原因原因说明虚假唤醒条件变量可能无缘无故醒来while会重新检查条件if会带着错误的状态往下跑丢失唤醒如果不持锁检查条件可能错过notify_one—— 带谓词的wait会自动处理这件事不带谓词的写法是错的_cv.wait(lock); // 错误的 醒来后不检查条件可能任务池是空的4. 为什么_stop要用std::atomicbool_stop被多个线程同时读写工作线程读、stop()写必须保证可见性和原子性std::atomicbool _stop; // 正确 bool _stop; // 错误 数据竞争工作线程可能永远看不到变化d.五个有待改进得问题问题 1main里push完立刻get等于没有并发for(int i 0; i 10; i) { std::futureint fu pool.push(Add, 11, i); std::cout fu.get() std::endl; // -- 立刻阻塞等待 }每个任务都是提交 -- 等它做完 -- 再提交下一个线程池退化成单线程顺序执行。 想真正看到并发应该先把任务都提交完再统一取结果std::vectorstd::futureint fus; for (int i 0; i 10; i) fus.push_back(pool.push(Add, 11, i)); // 先全部提交 for (auto f : fus) std::cout f.get() std::endl; // 再统一取这样 10 个任务能同时被多个工作线程并行处理。问题 2std::bind会拷贝参数丢失移动语义auto tmp_func std::bind(std::forwardF(func), std::forwardArgs(args)...);std::bind存参数时会decay退化——std::forward白写了传进来的右值会被拷贝而不是移动move-only 的参数比如unique_ptr、packaged_task根本传不进去直接编译报错。C14 起有更好的写法lambda 初始化捕获auto task std::make_sharedstd::packaged_taskreturn_type()( [func std::forwardF(func), args std::make_tuple(std::forwardArgs(args)...)]() mutable { return std::apply(func, std::move(args)); });std::apply 是 C17。你编译用 -stdc11所以 std::bind 是这个版本的标准做法不算错但要知道它的局限问题 3stop()可能丢弃剩余任务while(!_stop) // -- 工作线程在这里检查 { ... _cv.wait(lock, [this](){ return _stop || !_taskpool.empty(); }); tmp_taskpool.swap(_taskpool); ... }如果 _stop 在工作线程执行完一批任务、准备回到while判断时被置位而队列里还有新任务 ——它会直接退出循环那些任务永远不会被执行。这个丢任务是静默的不会报错。想保证停之前把活干完需要在退出前再清一次队列void entry() { for (;;) { std::vectorfunctor tmp; { std::unique_lockstd::mutex lock(_mutex); _cv.wait(lock, [this]{ return _stop || !_taskpool.empty(); }); tmp.swap(_taskpool); // 只有在要停 且 队列已空时才退出 if (tmp.empty() _stop) return; } for (auto t : tmp) t(); } }不过这里得 main 里是push 一个 get 一个退出时队列本来就是空的所以实际不会触发。问题 4stop()的 check-then-set 不是原子的if(_stop true) return; // -- 检查 _stop true; // -- 设置两个线程同时调stop()可能都通过检查。更好的写法是if (_stop.exchange(true)) return; // 一步完成检查并设置问题 5tmp_taskpool每轮循环都重建while(!_stop) { std::vectorfunctor tmp_taskpool; // --- 每次都重新分配内存把它移到循环外面就能复用已分配的内存swap 之后 vector 的容量会保留在另一方std::vectorfunctor tmp_taskpool; // 放外面 while(!_stop) { { ... tmp_taskpool.swap(_taskpool); } for (auto task : tmp_taskpool) task(); tmp_taskpool.clear(); }e.运行结果因为main是push 一个、get 一个所以顺序输出——这反而证明了future.get()的同步作用每次 get 都会阻塞到对应任务执行完。想验证多线程真的在跑可以在Add里打印线程 IDint Add(int a, int b) { std::cout 线程 std::this_thread::get_id() 处理任务\n; return a b; }然后用上面问题 1里那个先全部提交、再统一取结果的写法——你会看到任务被不同线程接手。f.小结设计价值shared_ptrpackaged_task让 move-only 对象能被std::function持有 自动管理生命周期swap到本地再执行锁只保护队列操作不保护任务执行避免串行化wait带谓词防虚假唤醒、防丢失唤醒atomicbool _stop多线程可见性析构调stop()RAII忘了手动停也不会泄漏线程

相关新闻

拯救者花屏报43错误别急着换主板:先软后硬排查指南

拯救者花屏报43错误别急着换主板:先软后硬排查指南

1. 拯救者花屏报43错误,为什么我第一反应不是换主板拯救者系列游戏本用久了,花屏和显卡报43错误算是比较典型的一类故障。很多人一看到屏幕出现彩色条纹、画面撕裂,或者设备管理器里显卡旁边挂个黄色感叹号、属性里写着“该设备有问题&#x…

2026/10/17 17:53:46 阅读更多 →
从自然语言到三维模型:text-to-cad 技术路线与工程实践实战

从自然语言到三维模型:text-to-cad 技术路线与工程实践实战

text-to-cad 这个词,翻译过来就是“文本转 CAD”。我第一次听到时,第一反应是“这又要画大饼了吧”。后来真的在电脑前跑了几轮,看着一句普通得不能再普通的需求变成一个带孔、带圆角、能导出的 STEP 文件,我还真在原地愣了几秒。…

2026/10/17 16:51:51 阅读更多 →
工业互联网与DCS:分层协作而非替代,边缘计算成关键交接区

工业互联网与DCS:分层协作而非替代,边缘计算成关键交接区

工业互联网和传统工控的关系,这几年在圈子里被讨论得非常多,尤其是每次行业展会或者技术交流会,总有人抛出那个经典问题:工业互联网会不会把DCS干掉?问这话的人,有做IT出身转行到工业领域的,也有…

2026/10/16 2:13:14 阅读更多 →

最新新闻

用Design.md构建符合产品风格的原型设计:设计令牌实战指南

用Design.md构建符合产品风格的原型设计:设计令牌实战指南

上周跟某团队聊原型评审,对方把三个页面的设计稿拍在我面前,主色蓝就出现了五种,没有两个颜色值相同。评审会开了一个小时,快一半时间在争论“这个蓝到底是不是我们品牌那个蓝”。原型阶段最让人心累的从来不是画得快不快&#xf…

2026/10/20 20:19:47 阅读更多 →
跨学科类比提升创意新颖度:IdeaScientist实践指南

跨学科类比提升创意新颖度:IdeaScientist实践指南

做创意类工作的人,最能共鸣的一句话大概是:我知道要创新,可我没有新东西。过去三年,我一直在跟各种“点子枯竭”问题搏斗。做过竞品扫描,做过用户访谈,也试过各种头脑风暴法,但方案的新颖度始终…

2026/10/20 20:19:47 阅读更多 →
给AI编程助手装上开发方法论:六阶段工作流设计复盘

给AI编程助手装上开发方法论:六阶段工作流设计复盘

最近我在重构自己日常使用的 AI 编程工作流时,越来越明显地感受到一个现象:AI 编程助手本身的能力已经不缺了,缺的是“约束”。就像给一个执行力超强但毫无章法的实习生配了个无限量的咖啡,你让他“写个登录模块”,他能…

2026/10/20 20:19:47 阅读更多 →
JDBC连接MySQL全链路详解:驱动配置、PreparedStatement、事务与连接池实战

JDBC连接MySQL全链路详解:驱动配置、PreparedStatement、事务与连接池实战

JDBC 这个东西,很多 Java 开发天天在用,但真要自己从零写一遍“Java 连 MySQL 并完成增删改查”,不少人反而会卡在几个很基础的环节上。驱动版本不一致、时区报错、连接池参数不会填、PreparedStatement 和 Statement 搞混,这些坑…

2026/10/20 20:19:47 阅读更多 →
MATLAB调用REFPROP 64位DLL避坑指南

MATLAB调用REFPROP 64位DLL避坑指南

简介:本资源是面向工程热物理、制冷空调、化学工程等领域科研与工程人员的MATLAB-REFPROP高效调用方案,专为64位Windows平台设计,解决MATLAB环境下调用NIST官方REFPROP热力学数据库时常见的环境配置复杂、接口编译困难、DLL加载失败等痛点。压…

2026/10/20 20:19:47 阅读更多 →
技能拆解与能力建模:从抽象描述到可执行单元的系统方法

技能拆解与能力建模:从抽象描述到可执行单元的系统方法

1. 从“skills”这个词说起:它到底指什么“skills”这个词看起来简单,但放在不同语境里,含义差别很大。在职场语境下,它指的是一个人完成某项任务所具备的能力单元;在技术社区里,它常常指代一组可复用的技能…

2026/10/20 20:18:46 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →