C++异步编程工具 async promise-future packaged_task等
特性std::async抽象级别高低中核心作用启动一个异步任务并返回future在线程间手动传递一个值或异常包装一个可调用对象,将其与future绑定线程管理自动(由运行时库决定)手动(需要自己创建和管理线程)手动(需要自己将任务对象传递给线程执行)耦合度任务的调用和执行紧密耦合值的“生产者”和“消费者”完全解耦任务的“定义”和“执行”解耦主要用例简单的“即发即忘”式异步调用复杂的线程间通信,事件
深入探讨 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
将“设置值”的动作和“获取值”的动作完全分离开来。你可以在一个线程中创建promise
和future
,然后将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() {
// 创建一个 promise
std::promise<std::string> data_promise;
// 从 promise 获取 future
std::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. 获取与任务关联的 future
std::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::async |
std::promise |
std::packaged_task |
---|---|---|---|
抽象级别 | 高 | 低 | 中 |
核心作用 | 启动一个异步任务并返回 future |
在线程间手动传递一个值或异常 | 包装一个可调用对象,将其与 future 绑定 |
线程管理 | 自动 (由运行时库决定) | 手动 (需要自己创建和管理线程) | 手动 (需要自己将任务对象传递给线程执行) |
耦合度 | 任务的调用和执行紧密耦合 | 值的“生产者”和“消费者”完全解耦 | 任务的“定义”和“执行”解耦 |
主要用例 | 简单的“即发即忘”式异步调用 | 复杂的线程间通信,事件驱动模型 | 任务队列,线程池实现 |
何时使用哪个?
-
优先选择
std::async
: 如果你的需求仅仅是“在后台运行这个函数,我稍后需要它的结果”,那么std::async
是最简单、最安全、最推荐的选择。它能避免很多手动管理线程的麻烦。 -
当需要精细控制时,使用
std::promise
: 如果你的结果不是由一个简单的函数调用产生的,而是由一系列复杂的事件或计算决定的,或者当设置值的线程和获取值的线程生命周期完全独立时,std::promise
提供了所需的灵活性。 -
构建任务系统时,使用
std::packaged_task
: 如果你需要创建一个任务队列,让一组工作线程去处理,或者需要将任务的创建和执行分离开来(例如,在主线程中创建任务,在工作线程中执行),std::packaged_task
是最合适的构建块。它是实现线程池的完美工具。 [13][15]
Learn more:
- cpp-notes/future-and-promise.md at master - GitHub
- 【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索(一)-阿里云开发者社区
- C++ 中async、packaged_task、promise 区别及使用原创 - CSDN博客
- C++ 并发操作的同步 - GuKaifeng’s Blog
- C++ async | how the async function is used in C++ with example? - EDUCBA
- async、packaged_task、promise、future的区别与使用 - L_B__
- std::async
- std::promise in C++ - GeeksforGeeks
- std::promise - cppreference.com
- Concurrency in C++ : Passing Data between Threads — Promise-Future - Medium
- C++ – 一文搞懂std::future、std::promise、std::packaged_task、std::async的使用和相互区别-StubbornHuang Blog
- Packaged Task | Advanced C++ (Multithreading & Multiprocessing) - GeeksforGeeks
- Making a Thread Pool in C++ from scratch - DEV Community
- Building a Thread Pool with C++ and STL - Coding Notes
- 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 类似,但它是可重用的。当所有线程都到达屏障点后,它们被释放,然后屏障会自动重置,可用于下一轮同步。这非常适合迭代式算法,其中每一步计算都需要所有线程同步。
更多推荐
所有评论(0)