C++异步编程工具 async promise-future packaged_task等

深入探讨 C++11 中引入的四个核心异步编程工具:std::async, std::future, std::promise, 和 std::packaged_task。它们共同构成了 C++ 现代并发编程的基础。

为了更好地理解,我们可以使用一个餐厅点餐的类比:

  • std::future (取餐凭证):这是你点餐后拿到的“小票”或“取餐号”。你拿着它,可以稍后去查询(wait)你的餐是否做好了,或者直接等待并取餐(get)。这个凭证本身不能做餐,只能用来获取最终的结果。
  • std::async (套餐服务):这是最省心的方式,就像点一个“全自动套餐”。你告诉柜台你要什么(调用函数),系统(C++运行时)会自动安排一位厨师(一个新线程)去做,并直接给你一个取餐凭证(future)。你什么都不用管,等着取就行了。
  • std::promise (后厨的承诺):这更像是后厨内部的沟通机制。假设一个厨师(生产者线程)向另一个服务员(消费者线程)承诺“我一定会把这道菜做出来”。厨师持有“承诺书”(promise),可以在菜做好后把结果放进去。服务员则持有与这份承诺书配对的“取餐凭gingzheng”(future)。这种方式将“承诺做”和“等待取”这两个动作在不同线程中分离开来。
  • std::packaged_task (打包好的任务单):这就像一张标准化的“任务卡片”,上面写明了要做什么菜(要执行的函数)以及附带了一张可撕下的取餐凭证(future)。你可以把这张任务卡片创建好,但先不交给任何厨师。之后,你可以把它交给任何一个有空的厨师(任何一个线程)去执行。这非常适合任务队列和线程池的场景。

在深入探讨之前,我们先执行一次搜索,以确保所有信息的准确性和时效性。


核心概念与关系

这四个工具都定义在 <future> 头文件中,它们的核心目标是实现线程间的同步和数据传递。

  • std::future 是统一的“结果接收端”。 [1][2]
  • std::async, std::promise, std::packaged_task 都可以看作是“结果的生产者”,它们都能创建一个与之关联的 std::future 对象,但创建和使用方式各不相同。 [3][4]

下面我们逐一详细解析。

1. std::future:未来的凭证

std::future 是一个对象,它代表了一个异步操作的最终结果。你可以把它想象成一个占位符,这个“坑”未来会被某个值或者一个异常填满。

主要操作:

  • get(): 等待异步操作完成,然后获取其结果。这个函数会阻塞当前线程直到结果可用。注意:get() 只能被调用一次
  • wait(): 阻塞当前线程,直到结果可用,但不获取结果。
  • wait_for(), wait_until(): 带超时的等待。
  • valid(): 检查 future 是否与某个共享状态相关联(即是否有效)。

std::future 本身不启动任何线程或任务,它纯粹是用来接收结果的。

2. std::async:最高级的异步任务启动器

std::async 是一个函数模板,它的作用是启动一个异步任务,并返回一个持有该任务结果的 std::future。 [5] 这是最简单、最直接的异步编程方式。

特点:

  • 高度封装: 你只需要提供一个可调用对象(如函数、lambda)及其参数,std::async 会负责线程的创建和管理。
  • 启动策略 (Launch Policy): 这是 std::async 的一个关键特性,可以通过第一个参数指定: [6][7]
    • std::launch::async: 强制在一个新线程中立即异步执行任务。
    • std::launch::deferred: 延迟执行。任务不会立即开始,而是在你对返回的 future 调用 get()wait() 时,才在调用者的线程中同步执行。
    • 默认(不指定或使用 std::launch::async | std::launch::deferred): 由C++运行时库根据系统负载等情况自行决定是创建新线程还是延迟执行。
  • 析构函数行为: 由 std::async 返回的 std::future 对象,如果在其任务完成前被销毁,其析构函数会阻塞直到任务执行完毕。 [3] 这是一个重要的陷阱,必须确保在 future 销毁前,其结果已经被获取或等待。

用法示例:

#include <iostream>
#include <future>
#include <thread>
#include <chrono>int long_computation(int input) {std::cout << "Thinking..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(2));return input * 10;
}int main() {// 启动一个异步任务std::future<int> result_future = std::async(std::launch::async, long_computation, 5);// 在主线程中做其他事情std::cout << "Main thread is doing other work." << std::endl;// 当需要结果时,调用get()// 这会阻塞,直到 long_computation 完成int result = result_future.get();std::cout << "The result is: " << result << std::endl;return 0;
}

3. std::promise:一个明确的承诺

std::promise 提供了一种在线程间手动设置值或异常的机制。 [8][9] 它和 std::future 是一对一的“推-拉”关系:promise 负责“推”入一个值,而 future 负责“拉”取这个值。 [1][10]

特点:

  • 解耦: promise 将“设置值”的动作和“获取值”的动作完全分离开来。你可以在一个线程中创建 promisefuture,然后将 promise 移动到另一个线程去设置值。
  • 显式控制: 你可以精确控制何时、何地设置值(set_value)或异常(set_exception)。
  • 一次性使用: 每个 promise 只能设置一次值或异常。 [9]

用法示例:

#include <iostream>
#include <future>
#include <thread>
#include <string>void worker_thread(std::promise<std::string> p) {try {// 模拟一些工作std::this_thread::sleep_for(std::chrono::seconds(2));// 工作完成,兑现承诺p.set_value("Data from worker thread");} catch (...) {// 如果发生异常,设置异常p.set_exception(std::current_exception());}
}int main() {// 创建一个 promisestd::promise<std::string> data_promise;// 从 promise 获取 futurestd::future<std::string> data_future = data_promise.get_future();// 启动工作线程,并将 promise 移交给它// promise 不能被拷贝,只能移动std::thread t(worker_thread, std::move(data_promise));// 主线程做其他事情std::cout << "Main thread waiting for data..." << std::endl;// 等待并获取结果std::string data = data_future.get();std::cout << "Received data: " << data << std::endl;t.join();return 0;
}

4. std::packaged_task:打包好的待执行任务

std::packaged_task 是一个类模板,它包装一个可调用对象(函数、lambda等),并允许其结果被异步地获取。 [11][12] 它像是一个将“任务”和“获取结果的凭证”捆绑在一起的包裹。

特点:

  • 任务与执行分离: packaged_task 将任务的定义和任务的执行分离开来。你可以先创建一个 packaged_task,在稍后的某个时间点,再把它交给一个线程去执行。
  • 线程池的基石: 这个特性使得 packaged_task 成为实现线程池等任务队列系统的理想工具。 [13][14] 你可以创建一个 packaged_task 队列,然后让工作线程从中取出任务并执行。
  • 自带 Future: 创建 packaged_task 后,可以立即通过 get_future() 方法获取与之关联的 future 对象。 [12]

用法示例:

#include <iostream>
#include <future>
#include <thread>
#include <functional>
#include <vector>
#include <queue>int calculate_sum(int a, int b) {return a + b;
}int main() {// 1. 打包一个任务std::packaged_task<int(int, int)> task(calculate_sum);// 2. 获取与任务关联的 futurestd::future<int> result_future = task.get_future();// 3. 将任务移动到线程中执行// packaged_task 也不能被拷贝,只能移动std::thread t(std::move(task), 10, 20);// 主线程等待结果int result = result_future.get();std::cout << "The sum is: " << result << std::endl;t.join();return 0;
}

总结与对比

特性std::asyncstd::promisestd::packaged_task
抽象级别
核心作用启动一个异步任务并返回 future在线程间手动传递一个值或异常包装一个可调用对象,将其与 future 绑定
线程管理自动 (由运行时库决定)手动 (需要自己创建和管理线程)手动 (需要自己将任务对象传递给线程执行)
耦合度任务的调用和执行紧密耦合值的“生产者”和“消费者”完全解耦任务的“定义”和“执行”解耦
主要用例简单的“即发即忘”式异步调用复杂的线程间通信,事件驱动模型任务队列,线程池实现

何时使用哪个?

  • 优先选择 std::async: 如果你的需求仅仅是“在后台运行这个函数,我稍后需要它的结果”,那么 std::async 是最简单、最安全、最推荐的选择。它能避免很多手动管理线程的麻烦。

  • 当需要精细控制时,使用 std::promise: 如果你的结果不是由一个简单的函数调用产生的,而是由一系列复杂的事件或计算决定的,或者当设置值的线程和获取值的线程生命周期完全独立时,std::promise 提供了所需的灵活性。

  • 构建任务系统时,使用 std::packaged_task: 如果你需要创建一个任务队列,让一组工作线程去处理,或者需要将任务的创建和执行分离开来(例如,在主线程中创建任务,在工作线程中执行),std::packaged_task 是最合适的构建块。它是实现线程池的完美工具。 [13][15]


Learn more:

  1. cpp-notes/future-and-promise.md at master - GitHub
  2. 【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索(一)-阿里云开发者社区
  3. C++ 中async、packaged_task、promise 区别及使用原创 - CSDN博客
  4. C++ 并发操作的同步 - GuKaifeng’s Blog
  5. C++ async | how the async function is used in C++ with example? - EDUCBA
  6. async、packaged_task、promise、future的区别与使用 - L_B__
  7. std::async
  8. std::promise in C++ - GeeksforGeeks
  9. std::promise - cppreference.com
  10. Concurrency in C++ : Passing Data between Threads — Promise-Future - Medium
  11. C++ – 一文搞懂std::future、std::promise、std::packaged_task、std::async的使用和相互区别-StubbornHuang Blog
  12. Packaged Task | Advanced C++ (Multithreading & Multiprocessing) - GeeksforGeeks
  13. Making a Thread Pool in C++ from scratch - DEV Community
  14. Building a Thread Pool with C++ and STL - Coding Notes
  15. Getting Started With C++ Thread-Pool | by Bhushan Rane | Medium

补充:C++20引入了一些新的异步编程工具

a. 协程 (Coroutines)

这是最具革命性的变化,它引入了一种全新的异步编程模型。协程可以看作是可以暂停和恢复的函数。

核心理念:与 std::async 启动一个可能阻塞的线程不同,协程可以在等待一个操作(如网络I/O)时非阻塞地挂起自身,让出执行权。当操作完成后,它可以从挂起的位置恢复执行。这使得单个线程能够高效管理成千上万的并发任务。
新关键字:引入了 co_await, co_yield, co_return 三个关键字来定义和控制协程的行为。
优势:能够以近似同步的方式编写逻辑清晰的异步代码,彻底告别“回调地狱”(Callback Hell)。 它非常适合I/O密集型应用,如高性能网络服务器。
状态:C++20 提供了协程的底层语言支持,但上层的高级封装仍在发展中,通常需要配合像 Boost.Asio 这样的库来发挥最大威力。

b. std::jthread

std::jthread 是对 std::thread 的一个安全、现代的替代品。 [8]

自动 join: jthread 的析构函数会自动调用 join(),这意味着你不再需要手动管理线程的生命周期,从而避免了忘记 join() 或 detach() 导致的程序终止问题。这是它相比 std::thread 最显著的优势。
协作式中断: jthread 内置了停止令牌 (stop token) 机制 (std::stop_source, std::stop_token)。你可以从外部请求一个 jthread 停止,而线程内部可以通过检查 stop_token 的状态来优雅地退出循环,实现了协作式的任务取消。

c. 同步原语:std::latch 和 std::barrier

这两个工具用于协调多个线程的执行时机。

std::latch (门闩): 这是一个一次性的同步点。 你可以初始化一个计数器,多个线程到达后使计数器减一,当计数器减到零时,所有在 wait() 处等待的线程被同时唤醒。它非常适合“等待所有工作线程准备就绪后,一起开始执行任务”的场景。
std::barrier (屏障): 与 latch 类似,但它是可重用的。当所有线程都到达屏障点后,它们被释放,然后屏障会自动重置,可用于下一轮同步。这非常适合迭代式算法,其中每一步计算都需要所有线程同步。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/89409.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/89409.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux-网络管理

网络管理1. 网络基础1.1 TCP/IP 协议栈&#xff08;四层模型&#xff09;1.2 网络设备配置与基础概念1.3 网络接口命名规则1.4 网络配置文件位置2. 常用网络配置命令2.1 查看网络接口信息2.2 配置 IP 地址2.3 启用/禁用网卡2.4 修改网卡 MAC 地址2.5 配置网卡的 MTU&#xff08…

Linux锁的概念及线程同步

目录 1.常见锁概念 死锁 死锁四个必要条件 避免死锁 避免死锁算法 2. Linux线程同步 条件变量 同步概念与竞态条件 条件变量函数 初始化 销毁 等待条件满足 唤醒等待 简单案例&#xff1a; 条件变量使用规范 1.常见锁概念 死锁 死锁是指在一组进程中的各个进程均占有不会释放的…

docker更换国内加速器-更换华为加速器2025-717亲测可用docker 拉取镜像出错

[rootlocalhost ~]# docker pull nginx Using default tag: latest Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)报错原因就是…

Unity VR多人手术模拟恢复2:客户端移动同步问题分析与解决方案

Unity VR多人手术模拟恢复2&#xff1a;客户端移动同步问题分析与解决方案 &#x1f3af; 问题背景 在开发基于Unity Mirror网络架构的VR多人手术模拟系统时&#xff0c;我们遇到了一个复杂的客户端移动同步问题&#xff1a; 主要操作者&#xff08;第一个客户端&#xff09;&a…

uni-app开发的页面跳转全局加载中

uni-app开发的页面跳转全局加载中首先需要下载插件创建加载中页面组件app.vue页面中监听跳转首先需要下载插件 https://ext.dcloud.net.cn/plugin?id20613 创建加载中页面组件 <!-- 全局自定义加载中 --> <template><view v-if"visible" class&qu…

XXE漏洞4-XXE无回显文件读取-PentesterLab靶场搭建

一.PentesterLab靶场搭建(实验环境搭建)介绍&#xff1a;PentesterLab 是一个全面的漏洞演示平台&#xff0c;但是它是收费的&#xff0c;我们这里只使用它的 xxe 演示案例。安装 PentesterLab 虚拟机:下载好镜像&#xff1a; 1.打开VMware新建虚拟机&#xff0c;选择典型就行。…

【机器学习】图片分类中增强常用方式详解以及效果展示

图片增强常用方式详解 引言 图片数据的质量和多样性对模型的训练效果起着至关重要的作用。然而&#xff0c;实际获取的图片数据往往存在数量不足、分布不均衡等问题。图片增强技术应运而生&#xff0c;它通过对原始图片进行一系列变换&#xff0c;生成更多具有多样性的图片&…

【URL 转换为PDF】HTML转换为PDF

1、方法1 pdfkit 安装依赖 # 安装 wkhtmltopdf&#xff08;系统级&#xff09; # Ubuntu/Debian sudo apt install wkhtmltopdf# macOS brew install wkhtmltopdf# Windows 下载安装&#xff1a;https://wkhtmltopdf.org/downloads.html# 安装 Python 库 pip install pdfkitimp…

单链表的定义、插入和删除

一、定义一个单链表 struct LNode{ //定义单链表节点类型ElemType data; //存放节点数据元素struct LNode *next; //指针指向下一个结点 }; //增加一个新节点&#xff1a;在内存中申请一个结点所需空间&#xff0c;并用指针p指向这个结点 struct LNode * p (struc…

Nextjs官方文档异疑惑

第一个区别&#xff1a;不同的页面对应的路由器设定&#xff01; 继续用 app 路由器&#xff08;推荐&#xff0c;Next.js 未来主流&#xff09; 路由规则&#xff1a;app 目录下&#xff0c;文件夹 page.tsx 对应路由。例如&#xff1a; app/page.tsx → 对应 / 路由&#xf…

突破AI模型访问的“光标牢笼”:长上下文处理与智能环境隔离实战

> 当AI模型面对浩瀚文档却只能处理零星片段,当关键信息散落各处而模型“视而不见”,我们该如何打破这堵无形的墙? 在自然语言处理领域,**输入长度限制**(常被称为“光标区域限制”)如同一个无形的牢笼,严重制约了大型语言模型(LLM)在真实场景中的应用潜力。无论是分…

AI 智能质检系统在汽车制造企业的应用​

某知名汽车制造企业在其庞大且复杂的生产流程中&#xff0c;正面临着棘手的汽车零部件质检难题。传统的人工质检方式&#xff0c;完全依赖人工的肉眼观察与简单工具测量。质检员们长时间处于高强度的工作状态&#xff0c;精神高度集中&#xff0c;即便如此&#xff0c;由于人工…

设计模式》》门面模式 适配器模式 区别

// 复杂子系统 class CPU {start() { console.log("CPU启动"); } } class Memory {load() { console.log("内存加载"); } } class HardDrive {read() { console.log("硬盘读取"); } }// 门面 class ComputerFacade {constructor() {this.cpu ne…

windows内核研究(驱动开发 第一个驱动程序和调试环境搭建)

驱动开发 第一个驱动程序 驱动的开发流程 1.编写代码 -> 生成.sys文件 -> 部署 -> 启动 -> 停止 ->卸载 // 编写我们的第一个驱动程序 #include<ntddk.h>// 卸载函数 VOID DrvUnload(PDRIVER_OBJECT DriverObject) {DbgPrint("我被卸载了\n"…

ABP VNext + 多级缓存架构:本地 + Redis + CDN

ABP VNext 多级缓存架构&#xff1a;本地 Redis CDN &#x1f4da; 目录ABP VNext 多级缓存架构&#xff1a;本地 Redis CDN一、引言 &#x1f680;二、环境与依赖 &#x1f6e0;️三、架构概览 &#x1f310;请求全链路示意 &#x1f6e3;️四、本地内存缓存层 &#x1…

RGBA图片格式转换为RGB格式(解决convert转换的失真问题)

使用convert转换的问题 OpenCV 的 cv2.cvtColor(…, cv2.COLOR_BGRA2GRAY) 会直接忽略 Alpha 通道的含义&#xff0c;将它当作第四个颜色通道来处理。 转换公式如下&#xff1a; gray 0.114*255 0.587*0 0.299*0 ≈ 29也就是说&#xff0c;即使 Alpha 为 0&#xff08;完全透…

Spring AI之Prompt开发

文章目录1 提示词工程1_核心策略2_减少模型“幻觉”的技巧2 提示词攻击防范1_提示注入&#xff08;Prompt Injection&#xff09;2_越狱攻击&#xff08;Jailbreaking&#xff09;3 数据泄露攻击&#xff08;Data Extraction&#xff09;4 模型欺骗&#xff08;Model Manipulat…

Java面试(基础篇) - 第二篇!

未看第一篇的&#xff0c;这里可以直达 Java面试(基础篇) - 第一篇 Integer对象可以用判断吗&#xff1f;为什么&#xff1f; 回答 不可以&#xff0c;因为 比较的是对象的实例&#xff08;内存地址&#xff09;&#xff0c;Integer是有一个缓存机制的&#xff0c;它会将-1…

【C# in .NET】11. 探秘泛型:类型参数化革命

探秘泛型:类型参数化革命 泛型是 C# 和.NET框架中一项革命性的特性,它实现了 “编写一次,多处复用” 的抽象能力,同时保持了静态类型的安全性和高性能。与 C++ 模板等其他语言的泛型机制不同,.NET 泛型在 CLR(公共语言运行时)层面提供原生支持,这使得它兼具灵活性、安…

菜单权限管理

菜单管理系统的整体架构1.Menu 菜单表2.role 角色表3.role_menu 角色菜 单关联表&#xff08;多对多 &#xff09;要找role_id为3的角色能用哪个菜单:SELECT *FROM sys_menu a LEFT JOIN sys_role_menu b ON a.menu_id b.menu_id WHERE role_id3拆分开就是4.user 用户表5.user…