ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Folly Futures 完全指南:用 Promise/Future 模式编写可组合的 C++ 异步代码

Folly Futures 完全指南:用 Promise/Future 模式编写可组合的 C++ 异步代码 Folly Futures 完全指南用 Promise/Future 模式编写可组合的 C 异步代码【免费下载链接】follyAn open-source C library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/follyFolly Futures 是 FacebookMeta开源 C 库 folly 中用于表达异步代码的框架核心采用 Promise/Future 模式。与std::future相比它最大的价值在于允许你通过thenValue/thenTry给 Future 挂接回调并在 Executor 的控制下管理回调执行位置从而用顺序与并行的组合方式写出干净、可维护的异步代码。读完本文你将掌握folly::Future与folly::SemiFuture的核心模型、回调链式组合、多 Future 聚合、执行器切换以及 Promise 的完整创建与兑现流程并能直接写出可运行的异步程序。概述Folly Futures 是什么Folly Futures 是一个受 Twitter 的 Scala 版 Futures 实现Finagle 框架启发的 C 异步框架同时松散地建立在 C11 标准库std::future与 Boost 1.53.0 的boost::future之上。虽然接口上受std::future启发但它不是std::future的替代品drop-in replacement——因为有些理念无法在保持 API 兼容的前提下平移过来。与std::future最核心的区别是在 Folly Futures 中你可以给 Future 挂接回调通过thenValue或thenTry并且回调的执行位置由 Executor 控制。这一能力使得 Future 可以顺序组合、并行组合从而写出更整洁的异步代码从根本上摆脱回调地狱。在阅读下文前先建立两个关键概念folly::FutureT携带一个 Executor 的 Future允许挂接链式回调continuation。folly::SemiFutureT不携带 Executor 的 Future是Future的一个安全子集——它不能直接挂回调必须先用via(executor)转换为Future。从源码结构看两者都私有继承自futures::detail::FutureBaseT见 Future.h。快速上手最小可运行示例文档给出的最小示例完整展示了 Promise → SemiFuture → Future → 回调 → 兑现 的完整生命周期#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注意观察两点其一Promiseint p通过getSemiFuture()产出SemiFutureint再经.via(executor)绑定执行器成为Futureint其二回调foo并不在挂接thenValue时立刻执行而是在p.setValue(42)兑现之后、由ThreadedExecutor调度执行最终get()阻塞等待整条链完成。核心概念Future 的状态与取值Folly 文档以一个简化的 Memcache 客户端接口来说明问题。同步 API 虽然简单但很容易写出慢代码传统异步回调 APIasync_get(key, std::functionvoid(GetReply) callback)虽能写出高性能代码但对复杂应用而言会退化成被称为回调地狱的意大利面条式代码。而基于 Future 的 API 长这样SemiFutureGetReply future_get(string key);SemiFutureGetReply或FutureGetReply是将来某个时刻会得到的GetReply的占位符。一个 Future 通常以未兑现incomplete/unfulfilled状态诞生fut.isReady() false fut.value() // 未就绪时调用会抛异常在未来某个时刻它被兑现fulfilled就可以访问其值fut.isReady() true GetReply reply fut.value();异常同样是 Future 的一等公民如果异步生产者抛出异常Future 表示的是异常而非值。此时fut.isReady() true fut.value() // 会重新抛出该异常从源码看value()要求isReady() true否则抛出FutureNotReady且value()有T、const T、T、const T四种重载以适配不同引用类别见 Future.h。什么算异常取决于 API 设计者的判断。在 Memcache 示例中SERVER_ERROR不抛异常、而是显式编码进GetReply对象而CLIENT_ERROR通常意味着库本身的 bug属于真正异常更适合以异常形式向上抛出。关键原则是所有异常条件尤其是没人预料到的偶发异常都要被捕获并能在调用栈更高层被统一处理。用 thenValue / thenTry / thenError 摆脱回调地狱在SemiFuture绑定 Executor 成为Future之后就可以单调地monadically挂接和链式组合回调了。文档中的完整示例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)追加一个接收T的 continuation。FutureT正常就绪时调用它若有异常则跳过该回调把异常传递给链上的下一个回调见 Future.h。测试用例中大量使用makeFutureWidget(23).thenValue(...)验证其行为见 ThenTest.cpp。.thenTry(cb)追加一个接收folly::TryT的回调。TryT同时封装了值与异常因此回调内部可以自行判断成败见 Future.h。.thenError(tag, cb)只在出现异常时执行正常值时被跳过。tag_tExceptionType{}模板参数用于按异常类型过滤不传 tag 时回调参数类型为folly::exception_wrapper见 Future.h。在 C17 下可以用全局内联变量folly::tagExceptionType直接传入无需显式构造tag_t。这个示例的核心思想是你可以把一个结果类型变换成另一个类型并且可以在链中连续变换未处理的错误会自动沿链传播。中间变量fut1、fut2当然都是可省略的直接.thenValue(...).thenValue(...)一路连写即可。使用.thenValue/.thenTry挂回调是惯用法它把所有代码聚合到一处从而避开回调地狱。聚合多个 Futurecollect 家族聚合aggregation是 Future 组合能力的第二块拼图你可以把多个 Future 聚合成一个新的 Future在新 Future 满足特定条件全部完成 / 任一完成 / 完成 N 个时完成。文档给出 Memcache 批量场景的三段示例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());结合 Future.h 的声明collect 家族的完整形态如下函数返回类型完成时机备注collectAll(first, last)SemiFuturestd::vectorTryT所有 Future 完成每个结果以TryT呈现异常被收集而不短路collect(first, last)SemiFuturestd::vectorT所有 Future 完成遇到第一个异常即短路故结果类型是vectorT而非vectorTryTcollectAny(first, last)SemiFuturestd::pairsize_t, TryT任一 Future 完成返回(完成索引, Try)多个同时完成时赢家不确定对运行在不同线程上的 Future 是线程安全的collectAnyWithoutException(first, last)SemiFuturestd::pairsize_t, T第一个无异常完成的 Future若全部带异常完成则返回最后一个异常collectN(first, last, n)SemiFuturestd::vectorstd::pairsize_t, TryT有 n 个 Future 完成索引指向原顺序但结果向量顺序任意非线程安全此外每个函数还提供接受单个集合对象的糖方法如collectAll(Collection c)以及接受可变参数个数的版本collectAll(Fs... fs)返回SemiFuturestd::tupleTryT1, TryT2, ...。还有collectXUnsafe系列——它返回内联inline执行的Future会擦除输入 Future 的 Executor官方注释建议逐步淘汰并改用collectX(...).via(e)显式指定执行器。对应的行为验证集中在 CollectTest.cpp。控制执行位置Executor 与 via第二个关键能力是把 Future 与 Executor 关联起来。Executor 规定了工作在哪里运行。概括地说给定一个 Executor你可以把SemiFuture转换成带 Executor 的Future也可以把某个 Executor 上的Future迁移到另一个 Executor 上。folly::ThreadedExecutor executor; SemiFutureGetReply semiFut mc.future_get(foo); FutureGetReply fut1 std::move(semiFut).via(executor);via在SemiFuture与Future上均有重载并支持可选的优先级参数via(Executor::KeepAlive executor, int8_t priority)见 Future.h 与 Future.h。线程安全须知Future 是部分线程安全的。Future::thenValue与Promise::setValue以及所有最终归结为这两个调用的变体可以被不同线程调用只要存在某种完整内存屏障Promise/Future 就可以在线程间迁移。但要注意你无法预知回调究竟在哪个线程上执行。看这个应尽量避免的例子// 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 调用thenValue时执行甚至x在 Thread A、y/z在 Thread B。setValue与then之间存在竞争唯一保证是两者中后执行的哪一个会运行回调。因此官方建议一律优先使用.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。相关行为可参见 ViaTest.cpp。阻塞等待wait 与 get事件驱动代码通常不需要等待 Future——所有后续动作都发生在 then 回调里。但如果你需要批量工作流发起一批异步操作然后在某个同步点等待全部完成就需要阻塞等待。wait()正是为此设计且可选超时SemiFutureT wait() /SemiFutureT wait() Future.hbool wait(HighResDuration dur) 带超时的版本返回bool表示是否在期限内就绪FutureT wait() /FutureT wait() 以及带超时版本Future.h与wait()相关的还有get()——它比value()多一个阻塞语义get()会先等待就绪再返回值SemiFuture的get()见 Future.hFuture的见 Future.h。此外folly::Try见 Try.h封装了值或异常的二元状态是回调链中传递结果的核心载体未捕获的异常以folly::exception_wrapper见 ExceptionWrapper.h的形式在链上传递。等待相关的专项测试集中在 WaitTest.cpp。创建并兑现 PromisesetValue / setException / setWith如果你在封装一个异步操作或向用户提供异步 API就需要创建Promise。每个 Future 都有对应的 Promise唯一例外是通过makeFuture()直接诞生的已就绪 Future。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 exceptionPromise的完整 API 位于 Promise.hgetSemiFuture()Promise.h取出SemiFutureT与getFuture()二者只能调用一次getFuture()Promise.h直接取出带内联执行器的FutureT官方注释建议优先使用getSemiFuture()再.via()显式指定执行器setValue(M value)Promise.h兑现值Unit类型有专门的空参重载setException(exception_wrapper ew)及其模板变体Promise.h兑现异常setWith(F func)Promise.h推荐做法接收一个函数并自动捕获其抛出的异常。setWith的惯用写法Promiseint p; p.setWith([]{ try { // do stuff that may throw return 42; } catch (MySpecialException const e) { // handle it return 7; } // 任何我们没捕获的异常都会由 setWith 替我们捕获 });凡是函数体可能抛异常的地方setWith都比裸setValue更安全——它把捕获异常 → 兑现异常这一机械步骤自动化了避免漏接异常导致 Future 永不就绪。相关专项测试见 PromiseTest.cpp。另外makeFuture()系列用于直接创建已就绪的 FuturemakeFutureT(t)包装已有值Future.hmakeFuture()返回就绪的FutureUnitFuture.h。这对测试与同步值转异步链的场景非常有用例如makeFutureWidget(23).thenValue(...)。源码导航关键文件与测试如果你想深入阅读实现或编写测试以下是核心入口Future.hFutureBase、SemiFuture、Future的定义thenValue/thenTry/thenError/via/wait/get及 collect 家族的声明与文档注释Future-inl.h模板方法的实现细节以及FutureBaseHelper等内部辅助Promise.hPromiseT的定义与getSemiFuture/getFuture/setValue/setException/setWithfutures/detail/Core.h 与 futures/detail/Core.cppFuture 状态机的核心实现Try.h 与 ExceptionWrapper.hTryT与exception_wrapper的载体类型futures/test/CollectTest.cpp、futures/test/ThenTest.cpp、futures/test/ViaTest.cpp、futures/test/PromiseTest.cpp、futures/test/WaitTest.cpp各能力的专项测试futures/test/Benchmark.cpp性能基准可用于评估不同组合方式的代价。总结Folly Futures 通过 Promise/Future 模式为 C 提供了现代化的异步编程体验Promise负责生产结果值或异常SemiFuture提供无执行器的安全结果占位Future在绑定 Executor 后支持thenValue/thenTry/thenError的链式组合collect 家族collectAll/collect/collectAny/collectAnyWithoutException/collectN提供多 Future 聚合via则让你精确掌控每个回调的执行线程。这套模型在保留 C 性能优势的同时把回调地狱重构为扁平、可读、异常安全的数据流。本文所述的完整示例与 API 语义均来自 folly/docs/Futures.md 及其对应的源码实现可直接在 folly 仓库中对照查阅与实验。【免费下载链接】follyAn open-source C library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/folly创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表