书接上文:https://blog.csdn.net/Mason___/article/details/161485948?spm=1001.2014.3001.5502
三、异步机制
线程间异步通信用于任务结果传递或非阻塞协作。
同步是互斥与条件等待(防竞态) ,**异步是任务结果回调(非阻塞通信)**;两者可独立或组合使用(如用 condition_variable 实现异步通知)。
异步通信(传递结果/非阻塞任务) :使用 **std::future / std::promise** 、**std::packaged_task** 和 **std::async**,这些通过"共享状态"在任务线程与调用线程间传递值/异常,无需显式加锁。
std::future、std::promise、std::packaged_task 和 std::async 是 C++11 引入的并发编程组件,共同用于异步任务的结果传递和线程间通信。
3.1 核心组件与作用
3.1.1 std::future
- 作用:表示异步操作的结果,提供访问结果的机制(如等待、查询或提取值)。
- 核心特性 :
- 状态管理 :未就绪(任务未完成)、就绪(结果可用)、无效(未绑定任务或已调用
get())。 - 常用方法 :
get():阻塞获取结果(仅一次调用)。wait():阻塞等待结果就绪。valid():检查是否关联有效任务。
- 状态管理 :未就绪(任务未完成)、就绪(结果可用)、无效(未绑定任务或已调用
- 局限性:
- 单向获取 :
std::future仅提供get()、wait()等方法,用于从异步操作中获取结果或等待完成,但它无法主动设置结果。 - 结果来源 :
std::future的结果必须由其他机制(如std::async、std::packaged_task或std::promise)生成并填充。若没有这些机制,std::future无法独立工作。
- 单向获取 :
- 示例:
cpp
#include <iostream>
#include <future>
#include <thread>
int main() {
std::future<int> fut = std::async([]() {
std::this_thread::sleep_for(std::chrono::seconds(2));
return 42;
});
qDebug() << "aaaaaa";
qDebug() << "bbbbbb";
std::future<int> fut2 = std::async([]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
return 51;
});
qDebug() << "cccccc";
qDebug() << QString("Result: %1").arg(fut2.get()); // 阻塞获取结果
qDebug() << QString("Result: %1").arg(fut.get());
qDebug() << "dddddd";
}
结果:
cpp
aaaaaa
bbbbbb
cccccc
"Result: 51"
"Result: 42"
dddddd
3.1.2 std::promise ( + std::future)
其核心作用是为一个线程(生产者)提供存储结果(值或异常)的机制,并通过关联的 std::future 对象让另一个线程(消费者)异步获取该结果。
3.1.2.1 作用
- 主动设置结果 :
std::promise是异步操作的"生产者",通过set_value()、set_exception()等方法主动设置结果或异常。 - 与
std::future绑定 :每个std::promise对象通过get_future()返回一个关联的std::future对象,后者用于在另一线程中获取结果。 - 线程间通信 :
std::promise允许一个线程设置值,另一个线程通过std::future获取值,实现跨线程的结果传递。
3.1.2.2 核心特性
- 结果设置 :
set_value(const T& value):设置结果值。set_exception(std::exception_ptr e):设置异常。
- 关联future :
get_future()生成关联的std::future(仅一次调用)。 - 生命周期管理 :若未设置结果,关联的
std::future会抛出std::future_error(broken promise异常)。
3.1.2.3 问题讲解
有人会问我直接用std::future不也能获取结果吗?为什么要std::promise配合?
第一,std::future 的结果必须由其他机制(如std::async、std::packaged_task或std::promise)生成并填充。若没有这些机制,std::future无法独立工作。- 第二,**
std::async**自动管理线程和结果传递,但灵活性较低(例如无法动态设置结果)。
cpp
auto fut = std::async([] { return 42; }); // 内部使用 std::promise 或类似机制
int result = fut.get();
std::packaged_task:将可调用对象与结果存储绑定,但仍需通过std::promise或类似机制设置结果。
cpp
std::packaged_task<int()> task([] { return 42; });
std::future<int> fut = task.get_future();
std::thread(std::move(task)).detach(); // 任务在线程中执行
int result = fut.get();
3.1.2.4 std::promise作用
- 控制结果的生产时机:
例如,在多线程任务中,生产者线程可能需要根据条件动态生成结果,而 std::promise 提供了灵活的设置接口。
cpp
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread producer([&prom] {
int result = compute_result(); // 动态计算结果
prom.set_value(result); // 主动设置结果
});
int value = fut.get(); // 消费者线程获取结果
producer.join();
- 传递异常:
std::promise 可以通过 set_exception() 将异常从生产者线程传递到消费者线程的 std::future。
cpp
std::promise<void> prom;
std::future<void> fut = prom.get_future();
std::thread worker([&prom] {
try {
risky_operation();
prom.set_value(); // 成功时设置空值
} catch (...) {
prom.set_exception(std::current_exception()); // 传递异常
}
});
fut.get(); // 若发生异常,会在此处重新抛出
worker.join();
-
解耦生产与消费 :
std::promise和std::future的分离允许生产者和消费者独立运行,无需直接共享数据,避免竞态条件。
3.1.2.5 总结
std::future是异步结果的"消费者",用于获取结果或等待完成。std::promise是异步结果的"生产者",用于主动设置结果或异常。- 二者配合 :
std::promise提供结果的生产接口,std::future提供结果的消费接口,共同实现线程间安全的结果传递。
通过这种设计,C++ 标准库提供了灵活且线程安全的异步编程模型,适用于复杂的多线程场景。
3.1.3 std::packaged_task ( + std::future)
std::packaged_task 是 C++11 引入的类模板,用于将可调用对象(函数、lambda、仿函数等)包装为异步任务,并通过 std::future 获取其返回值或异常。
3.1.3.1 模板定义
cpp
template<class R, class... Args>
class packaged_task<R(Args...)>;
R: 可调用对象的返回类型(支持任意类型,包括void)。Args...: 可调用对象的参数类型包(支持 0 个或多个参数)。- 示例:
cpp
std::packaged_task<int(int, int)> task([](int a, int b) { return a + b; });
3.1.3.2 内部组成
- 存储的任务(Stored Task): 封装可调用对象(函数、lambda 等),调用签名需与模板参数匹配。
- 共享状态(Shared State) : 存储任务结果或异常,通过
std::future访问,线程安全。
3.1.3.3 核心工作机制
- 包装任务 : 初始化
std::packaged_task,绑定可调用对象。 - 绑定
future: 调用get_future()获取关联的std::future。 - 执行任务 : 通过
operator()或make_ready_at_thread_exit()触发任务。 - 获取结果 : 通过
future::get()阻塞等待结果或异常。
3.1.3.4 成员函数详解
| 成员函数 | 功能 | 关键特性 |
|---|---|---|
| 构造函数 | 包装可调用对象 | 支持移动构造,无拷贝构造;默认构造无效(valid() == false) |
get_future() |
获取关联的 std::future |
每个 packaged_task 仅能调用一次,否则抛出 std::future_error |
operator()(Args...) |
同步执行任务 | 需传入匹配参数;执行后立即置共享状态为就绪 |
make_ready_at_thread_exit(Args...) |
延迟状态就绪至线程退出 | 适用于确保线程资源释放后再通知其他线程的场景 |
reset() |
重置任务状态 | 销毁旧共享状态,创建新状态;可重新调用 get_future() |
valid() |
检查有效性 | 默认构造/移动后/reset() 后的对象返回 false |
swap() |
交换两个任务对象 | 轻量级操作,仅交换内部指针 |
3.1.3.5 使用示例
【1】基本用法(Lambda 表达式)
cpp
#include <future>
#include <iostream>
int main() {
std::packaged_task<int(int, int)> task([](int a, int b) {
return a + b;
});
std::future<int> result = task.get_future(); // 绑定 future
task(2, 3); // 同步执行任务
std::cout << "Result: " << result.get() << std::endl; // 输出: 5
return 0;
}
意思是支持operator()纯同步,虽然用它的目的是去支持异步(结果与任务分离)。
【2】异步执行(结合 std::thread)
cpp
#include <future>
#include <iostream>
#include <thread>
int main() {
std::packaged_task<int(int, int)> task([](int a, int b) {
return a * b;
});
std::future<int> result = task.get_future();
std::thread t(std::move(task), 4, 5); // 移动任务到线程
t.join();
std::cout << "Result: " << result.get() << std::endl; // 输出: 20
return 0;
}
【3】重置任务
std::packaged_task::reset() 的核心作用是重置任务状态,使其可以重新执行并生成新的结果 。这一机制常用于需要重复执行异步任务的场景(如线程池任务调度),但需注意及时更新关联的 std::future 对象以避免访问失效状态。
cpp
#include <iostream>
#include <future>
#include <thread>
int task(int x, int y) {
return x + y;
}
int main() {
// 1. 创建 packaged_task 并绑定任务
std::packaged_task<int(int, int)> pt(task);
std::future<int> fut = pt.get_future(); // 获取关联的 future
// 2. 第一次执行任务(同步)
pt(2, 3);
std::cout << "First result: " << fut.get() << std::endl; // 输出 5
// 3. 重置 packaged_task
pt.reset(); // 销毁旧共享状态,创建新状态
fut = pt.get_future(); // 必须重新获取 future
// 4. 第二次执行任务(通过线程异步执行)
std::thread t(std::move(pt), 4, 5);
t.join();
std::cout << "Second result: " << fut.get() << std::endl; // 输出 9
return 0;
}
输出:
First result: 5
Second result: 9
3.1.3.6 与 std::async 和 std::function 的对比
| 特性 | std::packaged_task<R(Args...)> |
std::async |
std::function<R(Args...)> |
|---|---|---|---|
| 核心定位 | 可调用对象的异步包装器(绑定共享状态) | 异步执行函数的快捷工具 | 可调用对象的通用包装器(无异步) |
| 任务执行时机 | 手动控制(operator() 或线程) |
自动控制(立即或延迟) | 手动调用(operator()) |
| 共享状态 | 内置(与 future 绑定) |
自动创建(隐式关联) | 无 |
| 任务复用性 | 支持(reset() 后可重用) |
不支持(一次性任务) | 支持(可重复调用) |
| 灵活性 | 高(完全控制线程/时机) | 低(封装细节) | 中(仅包装,无异步) |
3.1.3.7 适用场景
- 手动控制异步任务:需自定义线程管理或任务调度(如线程池)。
- 任务传递:将任务对象传递到其他线程执行。
- 结果异步获取 :通过
std::future实现线程同步和结果获取。
总结 :std::packaged_task 是连接可调用对象与 C++ 异步编程体系的核心桥梁,适合需要精细控制任务执行的场景,而 std::async 更适合快速实现简单异步操作。
3.1.4 std::async
std::async 是 C++11 引入的异步任务工具,用于简化多线程编程,通过返回 std::future 对象实现异步结果获取和任务管理。
3.1.4.1 核心功能
-
异步执行任务
- 可能在新线程中执行(取决于启动策略),也可能延迟执行。
- 返回
std::future对象,用于获取结果或管理任务状态。
-
结果获取与异常处理
future.get():阻塞等待任务完成,返回结果或抛出任务中的异常。future.wait():仅阻塞等待任务完成,不获取结果。
-
任务状态查询
future.valid():检查future是否关联有效结果。future.wait_for(timeout)/future.wait_until(timepoint):超时或定时等待。
3.1.4.2 启动策略
| 策略 | 说明 |
|---|---|
std::launch::async |
强制在新线程中异步执行任务。 |
std::launch::deferred |
延迟执行,直到调用 future.get() 或 wait() 时在当前线程同步执行。 |
| 默认策略 | **未显式指定策略,等同于 `std::launch::async |
3.1.4.3 基础用法示例
【1】最简用法(无显式策略)
cpp
#include <iostream>
#include <future>
int compute(int a, int b) {
return a + b;
}
int main() {
auto future = std::async(compute, 2, 3); // 启动异步任务
std::cout << future.get() << std::endl; // 阻塞等待结果(输出 5)
return 0;
}
【2】显式指定启动策略
cpp
auto future_async = std::async(std::launch::async, compute, 2, 3); // 强制异步
auto future_deferred = std::async(std::launch::deferred, compute, 2, 3); // 延迟执行
future_deferred.get(); // 此时在当前线程同步执行 compute(2, 3)
【3】传递引用参数
需用 std::ref 包装引用,避免值拷贝:
cpp
#include <functional>
void modify(int& value) {
value = 100;
}
int main() {
int x = 0;
auto future = std::async(modify, std::ref(x)); // 传递引用
future.get();
std::cout << x << std::endl; // 输出 100
return 0;
}
【4】异常处理
异步任务中的异常会被 std::future 捕获,直到调用 get() 时重新抛出:
cpp
int risky_compute(int x) {
if (x < 0) throw std::runtime_error("x is negative");
return x * 2;
}
int main() {
auto future = std::async(risky_compute, -1);
try {
int result = future.get(); // 抛出 std::runtime_error
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl; // 捕获异常
}
return 0;
}
3.1.4.4 注意事项
1. 生命周期管理
std::future析构时,若任务未完成且未被get()或wait(),可能导致程序终止(取决于实现)。- 若需共享结果,使用
std::shared_future。
2. 线程复用
std::async不保证任务一定在新线程中执行(尤其是deferred策略或编译器优化)。
3. 性能开销
- 频繁创建小任务可能有额外开销,适合中高耗时任务。
3.1.4.5 典型应用场景
- 并行计算:同时执行多个独立任务(如图像处理中的多区域计算)。
- 异步 I/O:发起 I/O 操作后异步等待结果,避免阻塞主线程。
- 任务编排 :结合
future.wait_for实现超时控制(如监控任务是否超时)。
3.1.4.6 总结
std::async 是 C++ 中简化异步编程的核心工具,通过 std::future 提供结果获取和状态查询能力。合理使用启动策略和异常处理机制,可有效提升代码的并发性能和可维护性。
3.2 对比与选择
| 工具 | 适用场景 | 特点 |
|---|---|---|
std::promise |
需要显式设置值或异常,并手动管理线程间通信。 | 灵活,但需手动管理生命周期和线程同步。 |
std::packaged_task |
需要将函数包装为异步任务,并手动调度到线程。 | 适合复杂任务调度,但需显式移动任务对象。 |
std::async |
快速启动异步任务,无需关心线程管理。 | 简单易用,但默认行为可能因编译器实现而异(如 std::launch 策略)。 |
3.3 注意事项
线程安全:
std::promise的set_value()和set_exception()不是线程安全的,需确保同一对象不被多线程同时修改。std::future的get()只能调用一次,多次调用会导致未定义行为。
异常处理:
- 若
std::promise设置异常,std::future::get()会重新抛出该异常,需用try-catch捕获。
资源管理:
- 确保
std::promise在设置结果前未被销毁,否则std::future::get()会抛出std::future_error。
性能优化:
- 短任务避免使用
std::async,因线程创建开销可能超过任务执行时间。 - 高并发场景考虑使用线程池(如 Intel TBB 或自定义实现)。
3.4 总结
std::promise+std::future:适合需要显式控制值传递和线程同步的场景。std::packaged_task:适合需要包装函数并手动调度任务的场景。std::async:适合快速启动异步任务,简化代码的场景。
根据需求选择合适的工具,可以高效实现线程间通信和异步任务管理。