Folly Futures 深度指南:用 Promise/Future 模式在 C++ 中编写可组合的异步代码
2026/9/21 15:32:43 网站建设 项目流程
  • 跨平台
  • 前端

【免费下载链接】react-native-windows

A framework for building native Windows apps with React.

项目地址:https://gitcode.com/gh_mirrors/re/react-native-windows
点击查看免费下载

Folly Futures 是 Facebook(Meta)开源 C++ 库 Folly 中一套基于 Promise/Future 模式的异步编程框架,核心思想是"把回调挂在 Future 上、由 Executor 决定回调运行在哪里",从而将回调地狱式的手写异步代码,重构为顺序与并行均可组合的声明式链条。本文以仓库中保留的 vnext/external/folly/folly/docs/Futures.md 为主体,结合仓库内 folly 副本的 Try.h 等源码实现,完整讲解Future/SemiFuture/Promise的核心概念、thenValue/thenTry/via/collectAll等关键 API 的用法与线程语义,并对照 react-native-windows 中 AsyncActionQueue.cpp 的Mso::Future与 ImageViewManagerModule.cpp 的ReactPromise给出工程化落地的真实参照。读完本文,你将掌握用 Folly Futures 改造异步接口、组合并发任务、控制回调执行线程,以及正确封装Promise的完整方法论。

一、概述:Folly Futures 是什么

Folly Futures 是一套用 C++ 表达异步代码的框架,采用 Promise/Future 模式。它最初受到 Twitter Finagle 中 Scala 版Future/Promise实现的启发,并"松散地"建立在 C++11 标准库std::future与 Boostboost::future(尤其是 1.53.0 及以上版本)之上。尽管接口上受到std::future启发,它并不是std::future的即插即用替代品——因为有些思想在迁移到 C++ 时无法在不破坏 API 兼容性的前提下完美翻译。

std::future最本质的差异在于:你可以给Future挂载回调(thenValuethenTry),并在一个 Executor 的控制下决定这些回调在哪里运行。这使 Futures 可以顺序组合、并行组合,从而写出更整洁的异步代码:

  • std::future只解决"等待一个结果",组合能力弱,等待时往往需要阻塞线程;
  • Folly Futures 把"结果何时就绪"与"就绪后干什么、在哪干"解耦,回调可以跨线程自由调度。

从仓库内的 folly 副本可以看到,Futures 机制赖以生存的两块基石都被完整保留:

  • vnext/external/folly/folly/Try.h:Try<T>的底层实现,用Contains::VALUE / EXCEPTION / NOTHING三态枚举区分"持有值 / 持有异常 / 空";
  • vnext/external/folly/folly/Executor.h:Executor抽象,决定回调在哪个执行上下文(线程池、事件循环等)中运行。

二、快速上手:一个最小可运行示例

Futures.md给出的开篇示例最能说明问题。它演示了 Promise 与 Future 从创建、关联执行器、挂回调,到最终兑现(fulfill)的完整生命周期:

#include <folly/futures/Future.h> #include <folly/executors/ThreadedExecutor.h> using namespace folly; using namespace std; void foo(int x) { // do something with x cout << "foo(" << x << ")" << endl; } // ... folly::ThreadedExecutor executor; cout << "making Promise" << endl; Promise<int> p; Future<int> f = p.getSemiFuture().via(&executor); auto f2 = move(f).thenValue(foo); cout << "Future chain made" << endl; // ... now perhaps in another event callback cout << "fulfilling Promise" << endl; p.setValue(42); move(f2).get(); cout << "Promise fulfilled" << endl;

输出为:

making Promise Future chain made fulfilling Promise foo(42) Promise fulfilled

注意几点关键语义:

  1. p.getSemiFuture()产出SemiFuture<int>,尚未关联执行器;
  2. .via(&executor)SemiFuture升级为携带执行器的Future<int>,此后回调都会投递到executor上运行;
  3. thenValue(foo)追加延续回调foo会在 Promise 被setValue(42)兑现之后、于 executor 的线程上执行;
  4. move(f2).get()阻塞等待整条链完成,这在命令行小工具或测试里常用,事件驱动型应用通常不需要。

三、三种异步 API 形态的演进:同步、回调、Future

文档用一个简化的 Memcache 客户端接口贯穿全文,先给出同步版本:

using std::string; class MemcacheClient { public: struct GetReply { enum class Result { FOUND, NOT_FOUND, SERVER_ERROR, }; Result result; // The value when result is FOUND, // The error message when result is SERVER_ERROR or CLIENT_ERROR // undefined otherwise string value; }; GetReply get(string key); };

同步 API:调用get()时线程必须阻塞等待结果。实现简单,但用同步 API 非常容易写出慢代码——大量线程在 I/O 上空等。

传统异步回调 API:同样的操作换成回调签名:

int async_get(string key, std::function<void(GetReply)> callback);

调用后操作立即开始,完成后调用回调。用这种 API 可以写出高性能代码,但对非平凡的应用来说,代码会退化成一种特殊的"意大利面",业界称之为callback hell(回调地狱):嵌套回调、错误处理分散、控制流难以追踪。

Future 化 API

SemiFuture<GetReply> future_get(string key);

SemiFuture<GetReply>Future<GetReply>是"我们终将拿到的那个GetReply"的占位符。在文档接下来的大部分描述里,Future可以同时指folly::SemiFuturefolly::Future——因为前者是后者的一个安全子集(前者不携带执行器、跨线程语义更简单)。

一个 Future 通常以"未兑现"(unfulfilled / incomplete)状态诞生:

fut.isReady() == false fut.value() // will throw an exception because the Future is not ready

在未来的某个时刻,Future 被兑现,此时可以访问其值:

fut.isReady() == true GetReply& reply = fut.value();

异常也是 Future 的一部分

Futures 原生支持异常。如果异步生产者抛出了异常,你的 Future 表示的就是一个异常而非值:

fut.isReady() == true fut.value() // will rethrow the exception

"什么算异常"取决于 API 设计者的判断。在 Memcache 示例中:

  • SERVER_ERROR(服务器错误)不作为异常,而是显式编码在GetReply::Result里,因为它是可预期的业务状态;
  • CLIENT_ERROR(客户端库自身的 bug)作为异常抛出,因为这意味着库有缺陷,属于真正的"异常情况"。

这些是 API 设计者的判断取舍。重要的是:异常条件(尤其是没人预料到的偶发异常)必须被捕获,并能在调用栈更高层得到处理。Folly 用 Try.h 中的Try<T>封装"值或异常"两种结果——其内部以Contains::VALUEContains::EXCEPTION区分两种状态,这正是"Future 要么成功携带值、要么失败携带异常"这一模型在底层数据结构的直接体现。

四、组合:聚合多个 Future

异步编程中仅"发起一个操作、事后取值"还不够,还有两件重要的事,第一件是聚合(aggregate):定义一个"当部分或全部被聚合的 Future 完成后才完成"的新 Future。

文档给出三个典型场景:批量请求全部完成、批量请求中任意一个完成、以及"只想要一个成功值":

MemcacheClient mc; // 场景一:等待全部完成 vector<SemiFuture<GetReply>> futs; for (auto& key : keys) { futs.push_back(mc.future_get(key)); } auto all = collectAll(futs.begin(), futs.end()); // 场景二:等待任意一个完成 vector<SemiFuture<GetReply>> futs; for (auto& key : keys) { futs.push_back(mc.future_get(key)); } auto any = collectAny(futs.begin(), futs.end()); // 场景三:等待任意一个"成功"完成(忽略异常) vector<SemiFuture<GetReply>> futs; for (auto& key : keys) { futs.push_back(mc.future_get(key)); } auto anyv = collectAnyWithoutException(futs.begin(), futs.end());
  • all:当所有futs都完成时才完成(任一失败会体现为异常,语义上等价于"等待全部");
  • any:当futs中任意一个完成时即完成(注意:任意一个完成,包括以异常完成);
  • collectAnyWithoutException:当至少有一个成功值可用时完成,用于"只要有一个值就够"的场景;
  • 文档还提到collectN():当你只需要其中 N 个完成时就绪的场景。

这些聚合函数返回的all/any本身也是 Future,具体类型与用法以头文件为准。这一组 API 直接对应分布式系统中的"扇出(fan-out)/ 扇入(fan-in)"模式,是并发批处理的基石。

五、Executor 与 via:控制"回调在哪里跑"

第二件重要的事是:把 Future 与一个 Executor 关联起来。Executor 指明"工作在哪里运行",这是 Folly Futures 区别于std::future的核心能力。

总结一句话:给定一个 executor,你可以:

  • SemiFuture转换成携带该 executor 的Future
  • 把"在 executor A 上的Future"迁移成"在 executor B 上的Future"。
folly::ThreadedExecutor executor; SemiFuture<GetReply> semiFut = mc.future_get("foo"); Future<GetReply> fut1 = std::move(semiFut).via(&executor);

一旦挂上 executor,Future就允许你以 monadic(单子)的方式把延续(continuation)挂上去并链式组合:

SemiFuture<GetReply> semiFut = mc.future_get("foo"); Future<GetReply> fut1 = std::move(semiFut).via(&executor); Future<string> fut2 = std::move(fut1).thenValue( [](GetReply reply) { if (reply.result == MemcacheClient::GetReply::Result::FOUND) return reply.value; throw SomeException("No value"); }); Future<Unit> fut3 = std::move(fut2) .thenValue([](string str) { cout << str << endl; }) .thenTry([](folly::Try<string> strTry) { cout << strTry.value() << endl; }) .thenError(folly::tag_t<std::exception>{}, [](std::exception const& e) { cerr << e.what() << endl; });

这个例子略显刻意,但点明了核心思想:你可以把一个结果从一种类型变换成另一种类型,可以链式串联,且未处理的错误会沿链传播。中间变量当然可以省略,直接写成一条链。

三种延续回调的语义辨析

  • .thenValue(cb):为某个Future<T>追加一个接收T&&的延续。如果上游是错误,回调被跳过,错误直接传给下一个延续。这是最常用的"正常路径"写法;
  • .thenTry(cb):延续接收一个folly::Try<T>,它同时封装了值与异常,回调内可自行处理"成功分支"与"失败分支";
  • .thenError(tag_t<ExceptionType>{}, cb)跳过值、只在出现异常时运行ExceptionType模板参数作为标签类型用于按异常类型过滤——只有异常类型匹配(或可转换)时回调才触发。tag_t<ExceptionType>{}是可选的:不传 tag 时,回调参数类型为folly::exception_wrapper(见 ExceptionWrapper.h);在 C++17 下,也可以直接用全局内联变量folly::tag<ExceptionType>传入,无需显式构造。

这一组 API 构成了"类型安全、异常安全"的异步管线:正常数据流走thenValue,异常兜底走thenError,需要同时看到两种结果时用thenTry

六、阻塞等待:wait()

前面一直回避"等待 Future"的问题。事件驱动型代码可能永远不需要等待——所有后续动作都发生在 then 块里。但如果你要做批处理工作流(发起一批异步操作,然后在某个同步点等待它们全部结束),就需要阻塞等待。

// Futures 提供阻塞方法 wait(),可选的超时参数 fut.wait(); fut.wait(chrono::milliseconds(100)); // 带超时的等待

wait()阻塞当前线程直到 Future 就绪;带超时版本会在超时后返回,避免无限期挂起。对命令行工具、测试代码或"批次同步点"场景,wait()是简单直接的选择。

七、线程安全与回调执行线程的"竞态陷阱"

Futures 是部分线程安全的。Promise 或 Future 可以在线程间迁移,只要存在某种完整的内存屏障(full memory barrier)。具体而言:Future::thenValuePromise::setValue(以及所有最终归结为这两个调用的变体)可以从不同线程调用

但是,你要小心:回调到底在哪个线程执行,可能出乎你的意料。考虑下面这个"直接从 Promise 取 Future、不经过更安全的 SemiFuture"的例子(这也是通常应该避免的写法):

// Thread A Promise<Unit> p; auto f = p.getFuture(); // Thread B std::move(f).thenValue(x).thenValue(y).thenTry(z); // Thread A p.setValue();

这是合法的、技术上线程安全的。然而你必须意识到:你并不知道xyz会在哪个线程执行。可能性包括:

  • 全部在 Thread A 的p.setValue()调用处执行;
  • 全部在 Thread B 的f.thenValue调用处执行;
  • x在 Thread A 执行,而y/z在 Thread B 执行。

原因在于setValuethen之间存在竞态——谁后执行,谁就负责跑回调。唯一的保证是:两者之一一定会执行回调。

用 via 建立强控制

为了安全,应当优先使用.via。你可以链式串联多个.via,对"回调在哪里跑"建立非常强的控制:

std::move(aFuture) .thenValue(x) .via(e1).thenValue(y1).thenValue(y2) .via(e2).thenValue(z);

语义非常明确:

  • xaFuture所关联 executor 的上下文中执行;
  • y1y2e1的上下文中执行;
  • ze2的上下文中执行;
  • 如果z之后想回到原始上下文,需要再次调用via并传入原来的 executor。

这是 Folly Futures 相对传统回调模型最实用的价值之一:显式声明每个阶段的执行上下文,从根上消除"回调跑错线程"这类竞态 bug。这种"工作 + 执行器"解耦的设计思想在 react-native-windows 中同样可见——例如 AsyncActionQueue.cpp 中,Mso::Future<void>通过result.Then(m_executor, ...)把动作完成的后续处理显式投递到指定执行器(Mso::DispatchQueue)上,保证队列内动作串行执行、状态一致。

八、深入 Promise:如何正确封装异步操作

如果你在包装一个异步操作,或者向用户提供异步 API,那么你需要创建Promise。每个 Future 都有一个对应的 Promise——唯一的例外是那些"生来就已就绪"的 Future(由makeFuture()直接创建)。

Promise 的使用极其简单:创建一个 Promise,取出其 Future,然后用值或异常兑现它

值示例:

Promise<int> p; SemiFuture<int> f = p.getSemiFuture(); f.isReady() == false p.setValue(42); f.isReady() == true f.value() == 42

异常示例:

Promise<int> p; SemiFuture<int> f = p.getSemiFuture(); f.isReady() == false p.setException(std::runtime_error("Fail")); f.isReady() == true f.value() // throws the exception

注意这里的模式:p.getSemiFuture()而不是p.getFuture()——先产出不携带执行器的SemiFuture,把"决定回调在哪跑"的权力交给调用方(由调用方决定何时.via(...))。这是推荐的安全用法。

setWith:自动捕获异常的推荐写法

实践上推荐使用setWith,它接收一个函数并自动捕获函数内漏掉的异常

Promise<int> p; p.setWith([]{ try { // do stuff that may throw return 42; } catch (MySpecialException const& e) { // handle it return 7; } // Any exceptions that we didn't catch, will be caught for us });

setWith的价值在于:你只处理自己关心的异常类型,其余异常自动被捕获并写入 Promise(最终以异常形式呈现给 Future 的消费方)。这保证了"异常永不丢失",与文档反复强调的"异常必须在更高层被处理"原则一致。

工程化参照:react-native-windows 中的 Promise/Future 应用

Folly Futures 的 Promise/Future 思想在 react-native-windows 中有两处直接体现:

  1. Mso 库的Mso::Future/Mso::Promise:在 AsyncActionQueue.cpp 中,每个入队动作都伴随一个Mso::Promise<void>PostAction返回Mso::Future<void>;动作完成后entry.Result.SetValue(std::move(result))兑现 Promise——与本文p.setValue(...)的模式完全同构,只是Mso::Maybe<void>同时携带了"值或错误";
  2. TurboModule 的React::ReactPromise:在 ImageViewManagerModule.cpp 中,getSizeprefetchImage等原生方法以React::ReactPromise<T>作为参数向 JS 侧暴露异步接口,由原生代码在异步操作完成后兑现 Promise,JS 侧则以await/.then消费。这正是"用 Promise 包装异步操作、给用户提供异步 API"的框架级实践。

九、总结

围绕 Folly Futures,本文完整覆盖了以下能力闭环:

关注点核心 API / 概念关键要点
占位结果SemiFuture<T>/Future<T>未兑现时可isReady()探测;value()未就绪时抛异常
异常传播Try<T>setException异常是 Future 的一等公民,沿链传播到thenError
顺序组合thenValue/thenTry/thenError类型变换、值+异常双分支、按异常类型过滤
并行聚合collectAll/collectAny/collectAnyWithoutException/collectN全部 / 任意 / 任意成功值 / 前 N 个完成
线程控制Executor+via声明式指定每段回调的执行上下文,规避竞态
阻塞等待wait()(可带超时)批处理同步点专用,事件驱动代码可不用
封装异步 APIPromise+getSemiFuture+setValue/setException/setWithsetWith自动兜底捕获异常;优先从SemiFuture起步

一句话方法论:Promise封装生产者,用SemiFuture传递结果,用via绑定执行器,用thenValue/thenTry/thenError组合消费逻辑,用collect*做并发扇入,用wait()收敛批处理。这样写出的异步 C++ 代码,既有事件驱动的吞吐,又有同步代码的可读性,这正是 Folly Futures 在现代 C++ 异步领域经久不衰的原因。

想要继续深入,可以在仓库中研读:folly 副本的核心实现 Try.h、ExceptionWrapper.h 与 Executor.h,以及 react-native-windows 中的真实应用范例 AsyncActionQueue.cpp 与 ImageViewManagerModule.cpp。

  • 跨平台
  • 前端

【免费下载链接】react-native-windows

A framework for building native Windows apps with React.

项目地址:https://gitcode.com/gh_mirrors/re/react-native-windows
点击查看免费下载
上一篇:Notion SDK错误处理终极指南:7种常见错误类型与解决方案
下一篇:Go QML与OpenGL集成实战:高性能图形渲染

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询