C++11 扩展 --- 并发支持库(补充3)

在多线程编程中,我们经常需要从线程中获取任务执行的结果。比如启动一个线程压缩文件后,我们需要知道压缩后的文件名和大小。在 C++11 之前,获取线程返回值并不直观,通常需要借助指针、互斥锁和条件变量的组合,代码复杂且容易出错。
传统方式的痛点
先看看没有 future 库时,我们是如何获取线程返回值的:
#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 是一个类模板(class template),专门用于接收异步任务的返回值或异常,是 C++ 异步编程的核心组件。
简单来说,std::future 就像是一张类型安全的提货单:当我们通过 std::async、std::packaged_task 或 std::promise 启动一个异步任务时,系统会立刻返回一个与任务返回值类型匹配的 future<T> 对象;
我们不需要立即等待任务结束,而是可以在后续需要结果时,调用 future.get() 来获取返回值。如果任务尚未完成,get() 会阻塞等待;如果任务已完成,则直接返回结果;若任务抛出异常,get() 也会将异常抛出,让主线程可以安全捕获,实现了异步操作、结果获取与异常处理的一体化,比手动管理线程、共享内存和同步机制更简洁、更安全。【核心要点】
std::future 只能调用一次 get(),第一次调用后共享状态就会被释放,再次 get() 会导致未定义行为,想让多个线程获取结果要用 std::shared_future。【注意】【所以我们应该按需保存】
future 的核心组件
<future>库主要包含以下几个核心组件:
- std::future:获取异步操作结果的 "提货单"
- std::async:启动异步任务的便捷函数
- std::promise:用于主动设置异步操作结果
- std::packaged_task:封装可调用对象为异步任务
下面我们逐一了解这些组件的用法。
1. std::async:最简单的异步任务
std::async是启动异步任务最简便的方式,它会自动创建线程并运行任务,返回一个std::future对象供我们获取结果。
#include <iostream>
#include <thread>
#include <future>
#include <chrono>
int add(int x, int y) {
std::cout << "[add] : my thread id is " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟业务处理时间
return x + y;
}
int main() {
std::cout << "[main] : my thread id is " << std::this_thread::get_id() << std::endl;
std::cout << "========== 使用 async 开启一个处理任务, 返回 future 封装好的结果 ===========" << std::endl;
int x, y;
std::cout << "请输入要计算的数:" << std::endl;
std::cout << "x = "; std::cin >> x;
std::cout << "y = "; std::cin >> y;
std::future<int> result = std::async(add, x, y);
std::cout << "等待结果中..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "计算结果: " << result.get() << std::endl;
return 0;
}
lfz@U22:~/WorkSpace/myLearn/future$ ./main
[main] : my thread id is 127777395471296
========== 使用 async 开启一个处理任务, 返回 future 封装好的结果 ===========
请输入要计算的数:
x = 100
y = 230
等待结果中...
[add] : my thread id is 127777388885568
计算结果: 330
这段代码比传统方式简洁得多,我们不需要手动创建线程、管理内存,也不需要显式调用 join()。
所以 std::async 是 C++11 用来启动一个异步任务的函数,你给它传一个函数 / 可调用对象,它就会在后台自动运行,不用你自己创建 thread、不用 join、不用管线程生命周期。
它会直接返回一个 std::future,你后面用 future.get() 就能拿到返回值。
那为什么不需要主线程进行join呢?
因为 std::async 会返回一个 std::future 对象,而等待线程结束、获取返回值的逻辑已经被封装在 future.get() 内部,当我们调用 get() 时,如果【异步任务】所在线程还没有执行完毕,get() 会自动阻塞当前主线程,一直等待线程运行完成,这个行为本质上和 std::thread 的 join() 是一样的,相当于系统在底层帮我们自动完成了等待线程结束的操作,不需要我们再手动调用 join();而如果不调用 get(),只要 future 对象正常生命周期结束,相关的线程资源也会被合理管理,不会像直接使用 std::thread 忘记 join() 那样触发程序异常,所以使用 std::async 时,主线程不再需要显式写 join()。
启动策略
std::async提供了两种启动策略:
std::launch::async:立即创建新线程执行任务std::launch::deferred:延迟执行,直到调用 get () 时才在当前线程执行
#include <iostream>
#include <thread>
#include <future>
#include <chrono>
#include <sstream>
#include <string>
std::string showInfo(const std::string& name) {
// 获取线程 ID
std::thread::id tid = std::this_thread::get_id();
// 转成 string
std::ostringstream oss;
oss << tid;
std::string tid_str = oss.str();
std::string idInfo = "[" + name + "] 线程 ID: " + tid_str;
return idInfo;
}
int main() {
std::cout << "========== 使用 async 的不同策略开启一个处理任务 ===========" << std::endl;
std::future task1 = std::async(std::launch::async, showInfo, "异步任务");
std::string deferred_msg = "延迟任务";
std::future task2 = std::async(std::launch::deferred, showInfo, std::ref(deferred_msg));
std::cout << "[main] : 线程 ID: " << std::this_thread::get_id() << std::endl;
std::cout << "等待结果中..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << task1.get() << std::endl;
std::cout << task2.get() << std::endl;
return 0;
}
========== 使用 async 的不同策略开启一个处理任务 ===========
[main] : 线程 ID: 124725928223680
等待结果中...
[异步任务] 线程 ID: 124725921576512
[延迟任务] 线程 ID: 124725928223680
std::async 存在两种关键启动策略,会直接影响任务执行方式:std::launch::async 表示立即创建新线程并在后台异步执行任务,属于真正的异步;
std::launch::deferred 则不会创建线程,只会延迟执行,直到调用 get() 或 wait() 时才在当前线程同步运行函数,并非异步;
而默认不指定策略时,等效于同时使用两种策略,由系统自行选择,行为不确定,是容易踩坑的地方。
同时,无论采用哪种启动策略,我们都不需要手动调用 join(),因为使用 std::launch::async 时,get() 内部会自动阻塞等待线程结束,相当于完成了 join 的工作,使用 std::launch::deferred 时则直接在当前线程执行,根本不存在线程需要等待的问题,因此 std::async 结合 std::future 彻底省去了手动管理线程和 join 的操作。
2. std::promise:主动设置结果
在 C++11 引入的并发编程模型中,std::promise 与 std::future 是一对相辅相成的工具,它们共同构建了异步操作中 **“结果传递”的桥梁。简单来说,std::promise 负责 “生产” 结果,而 std::future 负责 “消费” 结果,二者通过一个隐藏的 “共享状态” 实现跨线程通信。
具体来说:
std::promise 和 std::future 本质上是一对一的生产者 - 消费者模型,一个 promise 只能对应一个 future,不支持一对多、多对多,所以不存在我们担心的 “多个生产覆盖数据”“多个线程重复消费” 的问题,因为标准库设计上就限制了只能写一次、只能读一次。它的底层共享状态需要保证:生产者写入后,消费者能被精准唤醒,并且等待时不占用 CPU,所以实现上不会用轻量的原子 + CAS,而是使用互斥锁保护共享状态,条件变量实现等待与唤醒,这样既能保证线程安全,又能让消费者在没有结果时进入休眠,避免空轮询,是最稳定、最符合 C++ 标准要求的实现方式。
虽然
std::promise和std::future是一对一、只写一次、只读一次,从逻辑上避免了多写、重复消费,但它们之间的共享状态不是原子操作,且future的阻塞等待必须靠条件变量实现,而条件变量本身强制依赖互斥锁才能安全等待与唤醒,所以底层依然必须用锁,锁是为了保证共享状态读写完整、等待唤醒机制可靠,不是为了防并发覆盖。
先看一个基础示例,感受它们的协作方式:
#include <iostream>
#include <thread>
#include <future>
int main() {
std::cout << "========== promise 与 future 生产消费关系 ===========" << std::endl;
std::promise<int> producer;
// 绑定生产方
std::future<int> consumer = producer.get_future();
std::thread t([&producer](){
// 模拟耗时操作(如网络请求、文件IO等)
std::this_thread::sleep_for(std::chrono::seconds(1));
// 任务完成后,通过promise设置结果,触发共享状态就绪[就是生产产品,通知消费]
producer.set_value(42);
});
// 主线程中,future 等待结果就绪并获取
std::cout << "等待异步结果..." << std::endl;
// get()会阻塞直到结果可用,且只能调用一次[因为绑定方只生产了一个产品]
std::cout << "获取到的结果: " << consumer.get() << std::endl;
// std::cout << "获取到的结果: " << consumer.get() << std::endl; // 再次就会报错
t.join();
return 0;
}
lfz@U22:~/WorkSpace/myLearn/future$ ./main
========== promise 与 future 生产消费关系 ===========
等待异步结果...
获取到的结果: 42
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 本质上就是封装好的、绑定一对一的条件变量 + 互斥锁 + 共享状态。
future.get()没数据时,就是condition_variable.wait(),阻塞休眠、不占 CPU;promise.set_value()就是把值写进共享状态,然后自动调用notify_one(),精准唤醒绑定的那个 future;- 它和你手写
mutex + condition_variable的底层逻辑一模一样,只是 C++ 把 “等待、唤醒、共享状态、锁” 全部打包好了,你不用自己写,更安全、更少出错。
一句话:future = 封装好的 waitpromise = 封装好的 notify_one它们就是语法糖版、安全版、一对一版的条件变量。
扩展:与其他异步工具的配合
std::promise与std::future是 C++ 异步编程的基础,它们还能与更高层次的工具配合使用:
std::packaged_task:将函数或可调用对象包装为一个 "任务",自动创建关联的future,本质是对promise的封装(任务执行完毕后自动调用set_value)。std::async:更简洁的异步接口,可直接启动异步任务并返回future,无需手动管理线程和promise,底层可能使用线程池优化性能。
例如,用std::async简化上述示例:
// 自动管理线程,返回的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 获取。
也就是说:std::packaged_task 就是封装了 promise 的任务包装器,它内部自动创建 promise、自动关联 future,我们只需要给它一个函数,它执行完函数后自动把返回值塞给 promise,我们通过 future.get() 拿结果就行。
基本语法结构
// 声明:包装返回类型为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...); // 转移到新线程执行
#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;
}
- 创建 packaged_task:把函数
compute_square_root包进去,内部自动生成一个 promise。 - 获取 future:
task.get_future()拿到和内部 promise 绑定的 future。 - 把任务扔到线程执行:线程启动后运行函数,计算出结果。
- 自动设置结果:函数返回值自动被 packaged_task 调用
promise.set_value(返回值)。 - 主线程 get ():阻塞等待 → 拿到结果。
- join 等待线程结束。
std::packaged_task非常适合那些需要重复执行的任务,我们可以像传递普通函数一样传递它。
future 的常用方法
std::future提供了一系列方法来管理和获取异步操作的结果:
get():获取结果,如果任务未完成则阻塞等待wait():等待任务完成,但不获取结果wait_for(duration):等待指定时长,返回等待状态wait_until(timepoint):等待到指定时间点,返回等待状态valid():检查 future 是否有效(是否关联到一个异步任务)
#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()时会重新抛出,让我们可以在主线程中统一处理异常:
#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。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)