一、介绍
std::future::then 是现代异步编程的核心模式,但是尚未成为 C++ 标准,但其设计思想至关重要。其表示从等待结果发送,到让其结果自动执行的转变。
1. 链式调用对比函数调用
函数调用
auto file = downloadFileAsync("url").get(); // 阻塞
auto dir = unzipFileAsync(file).get(); // 阻塞
auto data = parseDataAsync(dir).get(); // 阻塞
updateUI(data);
当我获取到上一个结果的时候,我的线程才会往下执行
链式调用
downloadFileAsync(...)
.then(unzipFileAsync)
.then(parseDataAsync)
.thenError([](std::exception_ptr e) {
// 上游任何一个环节出错,都会直接跳到这里来处理。
handleError(e);
});
总结对比
| 特性 | 链式调用 (f.then(...)) | 阻塞等待 (f.get()) |
|---|---|---|
| 线程行为 | 非阻塞。调用线程立即返回,可以继续工作。 | 阻塞。调用线程休眠,直到结果就绪。 |
| 适用场景 | UI 应用、高并发服务器、复杂的异步流程。 | 简单的同步点,或者当主线程确实需要等待子任务完成才能继续时。 |
| 代码结构 | 声明式的、可组合的异步工作流。 | 命令式的、线性的阻塞代码。 |
| 资源效率 | 高。线程不会空闲等待,与线程池配合完美。 | 低。线程在等待时被浪费。 |
| 错误处理 | 可链式处理,集中优雅。 | 分散,需要在每个 get() 处处理。 |
二、疑问QA
问 1:f.then() 链式调用相比 f.get() 阻塞等待,好在哪里?
答: 核心优势在于避免阻塞关键线程,从而提升程序的响应能力和吞吐量。
-
f.get()的问题:它会阻塞调用者线程。如果这个线程是 UI 线程,程序会冻结;如果是服务器的工作线程,该线程将被浪费,无法处理其他请求。它将异步操作强行拉回了同步。 -
f.then()的优势:- 非阻塞性:调用
.then()只是注册一个未来的计划,调用线程会立即返回,可以继续处理其他任务(如保持UI响应)。 - 清晰的工作流:可以轻松地将多个异步操作组合成一个逻辑清晰的“管道”,
a.then(b).then(c)。 - 高效的资源利用:与线程池结合,任务只在数据就绪时才被调度到工作线程上执行,避免了线程的空闲等待。
- 非阻塞性:调用
问 2:两种方式的“总耗时”差不多,且都有阻塞,为什么链式更好?
答: 这是一个非常深刻的问题。关键区别在于**“谁在阻塞”以及“阻塞的后果”**。
f.get()模式:是调用者线程(比如宝贵的 UI 线程)在阻塞。后果是灾难性的:UI 冻结,服务器吞吐量骤降。f.then()模式:阻塞(即 lambda 内部的f.get())发生在后台的工作线程上。这是可接受的,因为后台线程就是用来处理耗时任务的,它的阻塞不会影响关键线程的响应能力。
结论:.then() 的优越性不在于减少总耗时,而在于通过转移阻塞点,保护了关键线程不被阻塞。
问 3:.then() 的自动执行机制,和条件变量的“通知”类似吗?它是如何实现的?
答: 它们都是通知机制,但 future 是一个更高级、更强大的版本。
-
与条件变量的异同:
- 相似:都实现了“一个任务完成,通知另一个任务开始”的模式。
- 不同:条件变量是底层的无状态信令,只负责唤醒,不传递数据。
.then()是高层的有状态通道,它的“通知”不仅唤醒任务,还直接将上一步的结果作为数据传递给后续任务。
-
实现原理 (检测过程) :
- 注册:调用
.then(lambda)时,lambda函数(被称为连续体 anontinuation)被存储到future内部的共享状态 (Shared State) 对象中。 - 触发:当后台任务完成并调用
promise::set_value()时,这个函数被触发。 - 检测与执行:
set_value()的内部逻辑不仅会设置结果值,还会主动检查共享状态里是否存有已注册的lambda。如果检测到有,它就会将这个lambda和结果值打包,提交给一个执行器(如线程池)去运行。
- 注册:调用
这个检测不是轮询,而是在设置结果的那一刻由 promise 主动完成的事件驱动行为。