尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Folly Futures 深度指南:用 Promise/Future 模式在 C++ 中编写可组合的异步代码

Folly Futures 深度指南:用 Promise/Future 模式在 C++ 中编写可组合的异步代码 跨平台前端【免费下载链接】react-native-windowsA framework for building native Windows apps with React.项目地址https://gitcode.com/gh_mirrors/re/react-native-windows点击查看免费下载Folly Futures 是 FacebookMeta开源 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实现的启发并松散地建立在 C11 标准库std::future与 Boostboost::future尤其是 1.53.0 及以上版本之上。尽管接口上受到std::future启发它并不是std::future的即插即用替代品——因为有些思想在迁移到 C 时无法在不破坏 API 兼容性的前提下完美翻译。与std::future最本质的差异在于你可以给Future挂载回调thenValue或thenTry并在一个 Executor 的控制下决定这些回调在哪里运行。这使 Futures 可以顺序组合、并行组合从而写出更整洁的异步代码std::future只解决等待一个结果组合能力弱等待时往往需要阻塞线程Folly Futures 把结果何时就绪与就绪后干什么、在哪干解耦回调可以跨线程自由调度。从仓库内的 folly 副本可以看到Futures 机制赖以生存的两块基石都被完整保留vnext/external/folly/folly/Try.hTryT的底层实现用Contains::VALUE / EXCEPTION / NOTHING三态枚举区分持有值 / 持有异常 / 空vnext/external/folly/folly/Executor.hExecutor抽象决定回调在哪个执行上下文线程池、事件循环等中运行。二、快速上手一个最小可运行示例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; Promiseint p; Futureint 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注意几点关键语义p.getSemiFuture()产出SemiFutureint尚未关联执行器.via(executor)把SemiFuture升级为携带执行器的Futureint此后回调都会投递到executor上运行thenValue(foo)追加延续回调foo会在 Promise 被setValue(42)兑现之后、于 executor 的线程上执行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::functionvoid(GetReply) callback);调用后操作立即开始完成后调用回调。用这种 API 可以写出高性能代码但对非平凡的应用来说代码会退化成一种特殊的意大利面业界称之为callback hell回调地狱嵌套回调、错误处理分散、控制流难以追踪。Future 化 APISemiFutureGetReply future_get(string key);SemiFutureGetReply或FutureGetReply是我们终将拿到的那个GetReply的占位符。在文档接下来的大部分描述里Future可以同时指folly::SemiFuture或folly::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 中的TryT封装值或异常两种结果——其内部以Contains::VALUE与Contains::EXCEPTION区分两种状态这正是Future 要么成功携带值、要么失败携带异常这一模型在底层数据结构的直接体现。四、组合聚合多个 Future异步编程中仅发起一个操作、事后取值还不够还有两件重要的事第一件是聚合aggregate定义一个当部分或全部被聚合的 Future 完成后才完成的新 Future。文档给出三个典型场景批量请求全部完成、批量请求中任意一个完成、以及只想要一个成功值MemcacheClient mc; // 场景一等待全部完成 vectorSemiFutureGetReply futs; for (auto key : keys) { futs.push_back(mc.future_get(key)); } auto all collectAll(futs.begin(), futs.end()); // 场景二等待任意一个完成 vectorSemiFutureGetReply futs; for (auto key : keys) { futs.push_back(mc.future_get(key)); } auto any collectAny(futs.begin(), futs.end()); // 场景三等待任意一个成功完成忽略异常 vectorSemiFutureGetReply 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; SemiFutureGetReply semiFut mc.future_get(foo); FutureGetReply fut1 std::move(semiFut).via(executor);一旦挂上 executorFuture就允许你以 monadic单子的方式把延续continuation挂上去并链式组合SemiFutureGetReply semiFut mc.future_get(foo); FutureGetReply fut1 std::move(semiFut).via(executor); Futurestring fut2 std::move(fut1).thenValue( [](GetReply reply) { if (reply.result MemcacheClient::GetReply::Result::FOUND) return reply.value; throw SomeException(No value); }); FutureUnit fut3 std::move(fut2) .thenValue([](string str) { cout str endl; }) .thenTry([](folly::Trystring strTry) { cout strTry.value() endl; }) .thenError(folly::tag_tstd::exception{}, [](std::exception const e) { cerr e.what() endl; });这个例子略显刻意但点明了核心思想你可以把一个结果从一种类型变换成另一种类型可以链式串联且未处理的错误会沿链传播。中间变量当然可以省略直接写成一条链。三种延续回调的语义辨析.thenValue(cb)为某个FutureT追加一个接收T的延续。如果上游是错误回调被跳过错误直接传给下一个延续。这是最常用的正常路径写法.thenTry(cb)延续接收一个folly::TryT它同时封装了值与异常回调内可自行处理成功分支与失败分支.thenError(tag_tExceptionType{}, cb)跳过值、只在出现异常时运行。ExceptionType模板参数作为标签类型用于按异常类型过滤——只有异常类型匹配或可转换时回调才触发。tag_tExceptionType{}是可选的不传 tag 时回调参数类型为folly::exception_wrapper见 ExceptionWrapper.h在 C17 下也可以直接用全局内联变量folly::tagExceptionType传入无需显式构造。这一组 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::thenValue与Promise::setValue以及所有最终归结为这两个调用的变体可以从不同线程调用。但是你要小心回调到底在哪个线程执行可能出乎你的意料。考虑下面这个直接从 Promise 取 Future、不经过更安全的 SemiFuture的例子这也是通常应该避免的写法// Thread A PromiseUnit p; auto f p.getFuture(); // Thread B std::move(f).thenValue(x).thenValue(y).thenTry(z); // Thread A p.setValue();这是合法的、技术上线程安全的。然而你必须意识到你并不知道x、y、z会在哪个线程执行。可能性包括全部在 Thread A 的p.setValue()调用处执行全部在 Thread B 的f.thenValue调用处执行x在 Thread A 执行而y/z在 Thread B 执行。原因在于setValue与then之间存在竞态——谁后执行谁就负责跑回调。唯一的保证是两者之一一定会执行回调。用 via 建立强控制为了安全应当优先使用.via。你可以链式串联多个.via对回调在哪里跑建立非常强的控制std::move(aFuture) .thenValue(x) .via(e1).thenValue(y1).thenValue(y2) .via(e2).thenValue(z);语义非常明确x在aFuture所关联 executor 的上下文中执行y1、y2在e1的上下文中执行z在e2的上下文中执行如果z之后想回到原始上下文需要再次调用via并传入原来的 executor。这是 Folly Futures 相对传统回调模型最实用的价值之一显式声明每个阶段的执行上下文从根上消除回调跑错线程这类竞态 bug。这种工作 执行器解耦的设计思想在 react-native-windows 中同样可见——例如 AsyncActionQueue.cpp 中Mso::Futurevoid通过result.Then(m_executor, ...)把动作完成的后续处理显式投递到指定执行器Mso::DispatchQueue上保证队列内动作串行执行、状态一致。八、深入 Promise如何正确封装异步操作如果你在包装一个异步操作或者向用户提供异步 API那么你需要创建Promise。每个 Future 都有一个对应的 Promise——唯一的例外是那些生来就已就绪的 Future由makeFuture()直接创建。Promise 的使用极其简单创建一个 Promise取出其 Future然后用值或异常兑现它。值示例Promiseint p; SemiFutureint f p.getSemiFuture(); f.isReady() false p.setValue(42); f.isReady() true f.value() 42异常示例Promiseint p; SemiFutureint 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它接收一个函数并自动捕获函数内漏掉的异常Promiseint p; p.setWith([]{ try { // do stuff that may throw return 42; } catch (MySpecialException const e) { // handle it return 7; } // Any exceptions that we didnt catch, will be caught for us });setWith的价值在于你只处理自己关心的异常类型其余异常自动被捕获并写入 Promise最终以异常形式呈现给 Future 的消费方。这保证了异常永不丢失与文档反复强调的异常必须在更高层被处理原则一致。工程化参照react-native-windows 中的 Promise/Future 应用Folly Futures 的 Promise/Future 思想在 react-native-windows 中有两处直接体现Mso 库的Mso::Future/Mso::Promise在 AsyncActionQueue.cpp 中每个入队动作都伴随一个Mso::PromisevoidPostAction返回Mso::Futurevoid动作完成后entry.Result.SetValue(std::move(result))兑现 Promise——与本文p.setValue(...)的模式完全同构只是Mso::Maybevoid同时携带了值或错误TurboModule 的React::ReactPromise在 ImageViewManagerModule.cpp 中getSize、prefetchImage等原生方法以React::ReactPromiseT作为参数向 JS 侧暴露异步接口由原生代码在异步操作完成后兑现 PromiseJS 侧则以await/.then消费。这正是用 Promise 包装异步操作、给用户提供异步 API的框架级实践。九、总结围绕 Folly Futures本文完整覆盖了以下能力闭环关注点核心 API / 概念关键要点占位结果SemiFutureT/FutureT未兑现时可isReady()探测value()未就绪时抛异常异常传播TryT、setException异常是 Future 的一等公民沿链传播到thenError顺序组合thenValue/thenTry/thenError类型变换、值异常双分支、按异常类型过滤并行聚合collectAll/collectAny/collectAnyWithoutException/collectN全部 / 任意 / 任意成功值 / 前 N 个完成线程控制Executorvia声明式指定每段回调的执行上下文规避竞态阻塞等待wait()可带超时批处理同步点专用事件驱动代码可不用封装异步 APIPromisegetSemiFuturesetValue/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-windowsA framework for building native Windows apps with React.项目地址https://gitcode.com/gh_mirrors/re/react-native-windows点击查看免费下载相关推荐C线程池与异步编程ThreadPool中的future/promise应用C线程池与异步编程ThreadPool中的future/promise应用 1. 异步任务调度的痛点与解决方案 你是否在C开发中遇到过以下问题频繁创并发编程K8CScan与Cobalt Strike协同作战内网横向移动完整攻略K8CScan与Cobalt Strike协同作战内网横向移动完整攻略 K8CScan是一款专用于大型内网渗透的高并发插件化扫描神器集成信息收集、网络资产探Kitura异步编程模型Promise与Future应用Kitura异步编程模型Promise与Future应用 在Swift后端开发中异步编程是构建高性能服务器的核心能力。Kitura作为Swift生态中成熟的后端上一篇Notion SDK错误处理终极指南7种常见错误类型与解决方案下一篇Go QML与OpenGL集成实战高性能图形渲染创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表