
C++ 异步编程:用 future 库优雅获取线程返回值
在多线程编程中,我们经常需要从线程中获取任务执行的结果。比如启动一个线程压缩文件后,我们需要知道压缩后的文件名和大小。在 C++11 之前,获取线程返回值并不直观,通常需要借助指针、互斥锁和条件变量的组合,代码复杂且容易出错。
传统方式的痛点
先看看没有 future 库时,我们是如何获取线程返回值的:
cpp
#include<iostream>
#include<thread>
#include<mutex>
void fun(int x, int y, int* ans) {
*ans = x + y;
}
int main()
{
int a = 10;
int b = 8;
int* sum = new int(0);
std::thread t(fun, a, b, sum);
t.join();
// 获取线程的"返回值"
std::cout << *sum << std::endl; // 输出:18
delete sum;
return 0;
}
这段代码虽然能工作,但存在明显问题:
- 需要手动管理动态内存(new/delete)
- 必须显式调用 join () 等待线程完成
- 无法优雅地处理多个返回值
- 没有异常处理机制
如果线程需要在不同阶段返回多个结果,代码会变得更加复杂,需要更多的同步机制来协调线程间的数据传递。
什么是 future 库?
C++11 引入的<future>
头文件彻底改变了这种状况,它提供了一套完整的异步编程机制,让我们能以更简洁、安全的方式处理异步操作和获取结果。
简单来说,std::future
就像是一张 "提货单":当你启动一个异步任务时,它会立即给你返回一个 future 对象,你可以在需要结果的时候,用这张 "提货单" 来获取任务的结果。如果任务还没完成,它会耐心等待;如果任务已经完成,就直接返回结果。
future 的核心组件
<future>
库主要包含以下几个核心组件:
- std::future:获取异步操作结果的 "提货单"
- std::async:启动异步任务的便捷函数
- std::promise:用于主动设置异步操作结果
- std::packaged_task:封装可调用对象为异步任务
下面我们逐一了解这些组件的用法。
1. std::async:最简单的异步任务
std::async
是启动异步任务最简便的方式,它会自动创建线程并运行任务,返回一个std::future
对象供我们获取结果。
cpp
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
// 一个简单的任务函数,计算两数之和
int add(int a, int b) {
// 模拟耗时操作
std::this_thread::sleep_for(std::chrono::seconds(1));
return a + b;
}
int main() {
std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;
// 启动异步任务,立即返回future对象
std::future<int> result = std::async(add, 10, 20);
// 主线程可以继续做其他事情
std::cout << "等待结果中..." << std::endl;
// 获取结果,如果任务没完成会阻塞等待
std::cout << "10 + 20 = " << result.get() << std::endl;
return 0;
}
这段代码比传统方式简洁得多,我们不需要手动创建线程、管理内存,也不需要显式调用 join ()。
启动策略
std::async
提供了两种启动策略:
std::launch::async
:立即创建新线程执行任务std::launch::deferred
:延迟执行,直到调用 get () 时才在当前线程执行
cpp
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
void task(const char* name) {
std::cout << name << " 开始执行,线程ID: "
<< std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << name << " 完成" << std::endl;
}
int main() {
std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;
// 异步执行(新线程)
auto f1 = std::async(std::launch::async, task, "异步任务");
// 延迟执行(调用get时执行)
auto f2 = std::async(std::launch::deferred, task, "延迟任务");
std::cout << "主线程工作..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
f1.get(); // 等待异步任务完成
f2.get(); // 此时才执行延迟任务
return 0;
}
2. std::promise:主动设置结果
在 C++11 引入的并发编程模型中,std::promise
与std::future
是一对相辅相成的工具,它们共同构建了异步操作中 "结果传递" 的桥梁。简单来说,std::promise
负责 "生产" 结果,而std::future
负责 "消费" 结果,二者通过一个隐藏的 "共享状态" 实现跨线程通信。
先看一个基础示例,感受它们的协作方式:
cpp
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
int main() {
// 创建promise与对应的future,二者绑定到同一共享状态
std::promise<int> prom;
std::future<int> fut = prom.get_future();
// 启动线程执行异步任务,通过引用捕获promise
std::thread t([&prom]() {
// 模拟耗时操作(如网络请求、文件IO等)
std::this_thread::sleep_for(std::chrono::seconds(1));
// 任务完成后,通过promise设置结果,触发共享状态就绪
prom.set_value(42);
});
// 主线程中,future等待结果就绪并获取
std::cout << "等待异步结果..." << std::endl;
// get()会阻塞直到结果可用,且只能调用一次
std::cout << "获取到的结果: " << fut.get() << std::endl;
t.join();
return 0;
}
std::promise
的独特价值
std::promise
最强大的特性在于其 "主动性"------ 它允许我们在任意时机、任意线程中设置结果,而非局限于异步任务本身。这种灵活性使其适用于多种场景:
- 分阶段任务 :例如一个线程负责预处理数据,另一个线程在预处理完成后通过
promise
设置中间结果,主线程则通过future
获取并继续处理。 - 异常传递 :当异步操作发生错误时,可通过
promise.set_exception()
将异常存储到共享状态,future.get()
会在主线程中重新抛出该异常,实现跨线程异常安全传递。 - 外部事件触发 :比如等待用户输入、信号量触发等外部事件,完成后通过
promise
手动标记结果就绪。
与std::future
的核心区别
虽然二者总是成对出现,但职责边界清晰:
维度 | std::promise |
std::future |
---|---|---|
角色 | 结果的 "生产者" | 结果的 "消费者" |
核心操作 | set_value() /set_exception() (设置结果) |
get() /wait() (获取 / 等待结果) |
状态影响 | 主动将共享状态置为 "就绪" | 被动等待共享状态变为 "就绪" |
生命周期 | 通常与生产者线程绑定 | 通常与消费者线程绑定 |
复制性 | 不可复制,仅可移动(确保结果唯一设置) | 不可复制,仅可移动(结果只能被获取一次) |
需要注意的是,future.get()
是一次性操作 ------ 一旦调用,共享状态的结果会被 "取走",再次调用将导致未定义行为。如果需要在多个线程中获取同一结果,可使用std::shared_future
(通过future.share()
转换),它支持多次获取结果。
扩展:与其他异步工具的配合
std::promise
与std::future
是 C++ 异步编程的基础,它们还能与更高层次的工具配合使用:
std::packaged_task
:将函数或可调用对象包装为一个 "任务",自动创建关联的future
,本质是对promise
的封装(任务执行完毕后自动调用set_value
)。std::async
:更简洁的异步接口,可直接启动异步任务并返回future
,无需手动管理线程和promise
,底层可能使用线程池优化性能。
例如,用std::async
简化上述示例:
cpp
// 自动管理线程,返回的future直接关联任务结果
std::future<int> fut = std::async(std::launch::async, []() {
std::this_thread::sleep_for(std::chrono::seconds(1));
return 42;
});
std::cout << "结果: " << fut.get() << std::endl;
综上,std::promise
与std::future
构成了 C++ 异步编程的 "结果通道",前者赋予我们主动控制结果的能力,后者则提供了安全获取结果的机制。理解这对工具的协作模式,是掌握 C++ 并发编程的重要一步。
3. std::packaged_task:封装任务
std::packaged_task
是 C++11 引入的异步编程工具,它能将可调用对象(函数、lambda、函数对象等)包装起来,自动创建关联的 std::future
,当任务执行完毕后,结果会自动存储到共享状态中,供 future
获取。
基本语法结构
cpp
// 声明:包装返回类型为T,参数类型为Args...的可调用对象
std::packaged_task<T(Args...)> task(可调用对象);
// 获取关联的future
std::future<T> fut = task.get_future();
// 执行任务(两种方式)
task(args...); // 直接在当前线程执行
std::thread t(std::move(task), args...); // 转移到新线程执行
cpp
#include <iostream>
#include <future>
#include <cmath>
#include <thread>
// 计算平方根的函数
double compute_square_root(double x) {
return std::sqrt(x);
}
int main() {
// 封装任务
std::packaged_task<double(double)> task(compute_square_root);
// 获取future对象
std::future<double> result = task.get_future();
// 在新线程中执行任务
std::thread th(std::move(task), 25.0);
// 获取结果
std::cout << "25的平方根是: " << result.get() << std::endl;
th.join();
return 0;
}
std::packaged_task
非常适合那些需要重复执行的任务,我们可以像传递普通函数一样传递它。
future 的常用方法
std::future
提供了一系列方法来管理和获取异步操作的结果:
get()
:获取结果,如果任务未完成则阻塞等待wait()
:等待任务完成,但不获取结果wait_for(duration)
:等待指定时长,返回等待状态wait_until(timepoint)
:等待到指定时间点,返回等待状态valid()
:检查 future 是否有效(是否关联到一个异步任务)
cpp
#include <iostream>
#include <future>
#include <chrono>
int long_task() {
std::this_thread::sleep_for(std::chrono::seconds(3));
return 42;
}
int main() {
auto fut = std::async(long_task);
// 等待最多1秒
auto status = fut.wait_for(std::chrono::seconds(1));
if (status == std::future_status::ready) {
std::cout << "任务已完成,结果: " << fut.get() << std::endl;
} else if (status == std::future_status::timeout) {
std::cout << "等待超时,任务仍在执行..." << std::endl;
} else if (status == std::future_status::deferred) {
std::cout << "任务被延迟执行" << std::endl;
}
// 最终还是要获取结果
std::cout << "最终结果: " << fut.get() << std::endl;
return 0;
}
异常处理
异步任务中抛出的异常会被 future 捕获,当调用get()
时会重新抛出,让我们可以在主线程中统一处理异常:
cpp
#include <iostream>
#include <future>
#include <stdexcept>
void risky_operation() {
// 模拟一个可能失败的操作
throw std::runtime_error("操作失败: 资源不足");
}
int main() {
std::future<void> fut = std::async(risky_operation);
try {
fut.get(); // 可能会抛出异常
} catch (const std::exception& e) {
// 在主线程中处理异常
std::cout << "捕获到异常: " << e.what() << std::endl;
}
return 0;
}
这种机制确保了异步任务的异常不会被忽略,并且可以按照我们熟悉的方式处理。
总结
<future>
库为 C++ 异步编程提供了强大而优雅的解决方案:
- 简化了线程返回值的获取方式,无需手动管理同步机制
- 提供了多种异步任务的创建方式(async、promise、packaged_task)
- 内置了灵活的等待机制和异常处理
- 让代码更加清晰、简洁、易于维护
从传统的线程 + 指针 + 锁的复杂组合,到使用 future 库的简洁代码,C++ 的异步编程体验得到了质的飞跃。掌握 future 库,能让你在多线程编程中更加得心应手,编写出更高质量的并发代码。
在实际开发中,我们可以根据具体需求选择合适的组件:简单异步任务用std::async
,需要主动设置结果用std::promise
,封装可重用任务用std::packaged_task
。
async 与 future 的关系?
std::async
和 std::future
是 "生产者 - 消费者" 般的配套关系:std::async
负责 "生产" 异步任务的结果,std::future
负责 "消费" 这个结果,二者必须配合使用才能完成异步任务的 "启动 - 获取结果" 全流程。
简单说就是:你用 std::async
启动任务时,它会立刻给你一个 std::future
对象;后续你要拿任务结果,就靠这个 std::future
对象来获取。
🔗 核心关系:std::async
是 std::future
的 "创建者"
std::async
是 C++ 提供的 异步任务启动工具,它的核心作用有两个:
- 启动一个异步任务(可能新建线程,也可能延迟执行);
- 自动创建并返回一个
std::future
对象,这个对象直接关联到该异步任务的结果。
你可以把它们的关系理解成 "外卖下单":
- 你用
std::async
就像 "下单点外卖"------ 发起一个需求(异步任务); - 平台返回的 "订单号" 就是
std::future
------ 凭这个订单号,你能查进度(等待任务)、拿外卖(获取结果)。
看一段直观的代码就能明白:
cpp
// 1. 用 std::async 启动异步任务,它会返回一个 std::future 对象
std::future<int> fut = std::async([](){
// 异步任务:模拟耗时计算
std::this_thread::sleep_for(1s);
return 100; // 任务结果
});
// 2. 用 std::future 对象获取结果(没完成就等,完成了就拿)
int result = fut.get();
std::cout << "任务结果:" << result << std::endl; // 输出 100
📌 为什么不能单独用?
二者谁也离不开谁,少了一个都无法完成 "异步获取结果" 的需求:
- 只有 std::async,没有 std::future:你启动了任务,但拿不到结果 ------ 就像下单后没要订单号,根本不知道怎么取外卖;
- 只有 std::future,没有 std::async :你手里有个 "空的订单号"(默认构造的
std::future
是无效的),根本关联不到任何任务 ------ 没法查进度、没法拿结果。
🆚 和其他 future 来源的区别
除了 std::async
,std::future
还能通过 std::promise
和 std::packaged_task
创建,但 std::async
是最 "省心" 的方式:
来源(生产者) | 特点 | 适用场景 |
---|---|---|
std::async |
自动创建线程 / 管理任务,直接返回 future |
简单异步任务,不想手动管理线程 |
std::promise |
手动设置结果(set_value ),需自己创建线程 |
需要在任务中途 / 多个地方控制结果 |
std::packaged_task |
封装可调用对象(函数 /lambda),需自己传线程 | 需重复使用任务逻辑,或手动管理线程池 |
但无论哪种来源,std::future
的角色都是固定的 ------异步结果的 "接收者" 和 "访问接口"。
std::async 与 std::future 关系对比表
该表格从职责定位、核心能力、配合流程等维度,清晰梳理二者的关联与区别,帮你快速掌握它们的协作逻辑。
对比维度 | std::async(异步任务启动器) | std::future(异步结果接收器) | 二者关联说明 |
---|---|---|---|
核心职责 | 发起异步任务,是结果的 "生产者" | 存储 / 获取异步结果,是结果的 "消费者" | 生产者与消费者的配套关系,缺一不可 |
创建方式 | 直接调用函数(如 std::async(任务函数) ) |
由 std::async /std::promise /std::packaged_task 自动生成 |
不能手动创建有效实例,必须通过其他组件 "赠送" |
核心能力 | 1. 启动任务(支持立即 / 延迟两种策略)2. 自动绑定任务与 future3. 隐式管理线程生命周期 | 1. get() :阻塞获取结果(仅能调用一次)2. wait() :阻塞等待任务完成(不拿结果)3. wait_for() :限时等待并返回状态 |
async 启动任务后,必须通过其返回的 future 才能拿到结果 |
配合流程 | 步骤 1:调用 std::async 传入任务,生成 future步骤 2:async 内部启动 / 托管任务执行 |
步骤 3:调用 future 的 get() /wait() 等待结果步骤 4:获取任务返回值或捕获异常 |
流程环环相扣,少一步都无法完成异步结果获取 |
生命周期 | 任务执行完后自动结束(或随主线程退出) | 调用 get() 后变为无效(valid() 返回 false) |
future 失效后不可再用,async 任务完成后也无法重启 |
常见错误 | 1. 忽略返回的 future(任务会被阻塞执行)2. 重复调用启动同一任务 | 1. 对无效 future 调用 get() (崩溃)2. 多次调用 get() (崩溃) |
promise 是不是封装了 future ?
实际上std::promise
与std::future
并非 "封装" 关系,而是互补的协作关系 ------ 它们就像一根管道的两端:promise
是写入端,future
是读取端,共同操作同一个 "共享状态"(shared state)。
可以用一个生活场景类比:
- 你(主线程)让朋友(子线程)去买咖啡,递给朋友一个空杯子(
promise
) - 朋友买到咖啡后,把咖啡倒进杯子里(
promise.set_value()
) - 你拿着杯子的 "取货凭证"(
future
),等朋友把咖啡装好后,通过凭证拿到咖啡(future.get()
)
这里的关键是:杯子(共享状态)才是核心载体 ,promise
和future
只是操作这个载体的两个接口。
从实现角度看二者的关系
C++ 标准并未规定promise
和future
的具体实现细节,但逻辑上它们的关系是这样的:
cpp
+------------------------+
| 共享状态 |
| (存储结果/异常/状态) |
+----------+-------------+
|
+-------+-------+
| |
+--v--+ +--v--+
|promise| |future|
|(写接口)| |(读接口)|
+------+ +------+
promise
拥有对共享状态的 "写权限"(设置结果 / 异常)future
拥有对共享状态的 "读权限"(获取结果 / 异常)- 两者通过
promise.get_future()
建立关联,形成唯一的读写对
为什么容易误解为 "封装"?
可能是因为std::packaged_task
或std::async
这类工具内部封装了 promise,让我们无需直接操作它:
cpp
// packaged_task内部包含了一个promise
std::packaged_task<int()> task([]{ return 42; });
std::future<int> fut = task.get_future(); // 这里的future依然关联内部promise的共享状态
但这是更高层工具对 promise 的封装 ,而非future
封装promise
。本质上,promise
和future
始终是平等的协作关系,谁也不包含谁。
这种设计的精妙之处在于解耦生产者和消费者:
- 生产者只需知道如何通过
promise
设置结果,无需关心谁会使用这个结果 - 消费者只需知道如何通过
future
获取结果,无需关心结果由谁产生 - 两者甚至可以在不同的线程、不同的时间点创建,只要能关联到同一个共享状态即可
理解这种 "读写分离" 的设计,就能更清晰地把握 C++ 异步编程的核心思想了。
后面我们可以结合这些知识语法,自己实现一个案例!