多线程-std::future::then 链式调用

156 阅读4分钟

一、介绍

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() 的优势

    1. 非阻塞性:调用 .then() 只是注册一个未来的计划,调用线程会立即返回,可以继续处理其他任务(如保持UI响应)。
    2. 清晰的工作流:可以轻松地将多个异步操作组合成一个逻辑清晰的“管道”,a.then(b).then(c)
    3. 高效的资源利用:与线程池结合,任务只在数据就绪时才被调度到工作线程上执行,避免了线程的空闲等待。

问 2:两种方式的“总耗时”差不多,且都有阻塞,为什么链式更好?

答: 这是一个非常深刻的问题。关键区别在于**“谁在阻塞”以及“阻塞的后果”**。

  • f.get() 模式:是调用者线程(比如宝贵的 UI 线程)在阻塞。后果是灾难性的:UI 冻结,服务器吞吐量骤降。
  • f.then() 模式:阻塞(即 lambda 内部的 f.get())发生在后台的工作线程上。这是可接受的,因为后台线程就是用来处理耗时任务的,它的阻塞不会影响关键线程的响应能力。

结论.then() 的优越性不在于减少总耗时,而在于通过转移阻塞点,保护了关键线程不被阻塞

问 3:.then() 的自动执行机制,和条件变量的“通知”类似吗?它是如何实现的?

答: 它们都是通知机制,但 future 是一个更高级、更强大的版本。

  • 与条件变量的异同

    • 相似:都实现了“一个任务完成,通知另一个任务开始”的模式。
    • 不同:条件变量是底层的无状态信令,只负责唤醒,不传递数据。.then() 是高层的有状态通道,它的“通知”不仅唤醒任务,还直接将上一步的结果作为数据传递给后续任务。
  • 实现原理 (检测过程)

    1. 注册:调用 .then(lambda) 时,lambda 函数(被称为连续体 anontinuation)被存储到 future 内部的共享状态 (Shared State) 对象中。
    2. 触发:当后台任务完成并调用 promise::set_value() 时,这个函数被触发。
    3. 检测与执行set_value() 的内部逻辑不仅会设置结果值,还会主动检查共享状态里是否存有已注册的 lambda。如果检测到有,它就会将这个 lambda 和结果值打包,提交给一个执行器(如线程池)去运行。

这个检测不是轮询,而是在设置结果的那一刻由 promise 主动完成的事件驱动行为。