Folly Fibers Async 注解框架:用 Async<>/await 让纤维并发显式化

发布时间:2026/9/10 8:24:11
Folly Fibers Async 注解框架:用 Async<>/await 让纤维并发显式化 Folly Fibers Async 注解框架用 Async/await 让纤维并发显式化【免费下载链接】follyAn open-source C library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/folly导读folly/fibers/async是 Facebook 开源的 C 基础库 Folly 中为folly/fibers打造的一套语法注解框架通过Async返回值包装与await解包把纤维fiber中隐式的、难以察觉的阻塞与并发切换变得显式可见并借助编译期零开销包装与调试期运行时检查阻止阻塞函数意外跑在主线程栈main context上、阻止看似串行、实则并发的 I/O 扇出。阅读本文后你将掌握Async/await/init_await三大原语、promiseWait/baton_wait/futureWait/taskWait四类阻塞转换 API、collectAll扇出与executeOnFiberAndWait入口工具以及将整个既有 fiber 代码库渐进注解、最终向folly::coro迁移的完整工作流。该库面向已经使用folly/fibers的项目如果你在寻找一个全新的异步框架官方建议直接使用folly::coro。背景纤维的隐式阻塞为什么是问题Folly Fibers 的设计目标是让纤维表现得像线程但通过用户态上下文切换更轻量并且与 folly 的 executor 与 future 体系深度集成。当纤维遇到阻塞操作例如等待Baton、调用Future::get()时运行它的底层线程会被释放转而去调度其他纤维。问题在于这些阻塞操作在代码中既不明显也不显式。仅仅阅读某个函数体很难判断它是否会阻塞——除非仔细审查它的全部子调用。文档 folly/fibers/async/README.md 对此有一针见血的对比阻塞的线程只是等待虽然低效、可能死锁但行为可预测而阻塞的纤维会让出 CPU 去切换另一条纤维——这就在系统中引入了看不见的并发。由此产生两类典型 bug栈悄然切换阻塞代码路径可能无声地切回主线程栈执行导致本应像多线程一样并行的代码一旦阻塞就退化成单线程串行循环内串行 I/O阻塞函数在 for 循环中被迭代执行I/O 变成顺序进行而非扇出并发例如在循环里await无意中把并发 I/O 排成了串行。注解框架正是为了让这种纤维并发显式化从而消除上述隐患。设计目标两个核心用例与已知限制用例一提升必须留在纤维上的性能敏感应用的开发体验注解提供了以下收益详见 README.md提供熟悉的Async/await语法显式标记哪些函数可能阻塞调试模式强制检查阻止阻塞函数跑在主线程栈main context上从而避免意外的、隐式的线程级阻塞 I/O帮助不熟悉代码库的开发者避免在看似串行的代码里意外调度并发 I/O典型如 for 循环中的await强制 I/O 路径与非 I/O 路径清晰分离避免积累难以偿还的技术债。用例二作为迁移到 folly::coro 的安全中间步骤注解是迁移到folly::coro的安全且零成本的过渡台阶异步代码路径已经被识别并显式化所需的 I/O / 非 I/O 代码路径分离已经完成调试模式下会阻止在 coro 上下文里调用已注解的纤维感知代码避免隐式阻塞线程从Async/await到folly::coro的转换可以至少部分自动化——但要注意 eager-return 语义与引用传递等差异。已知限制文档明示所有阻塞操作都需要人工识别框架对未注解的函数不提供任何保护注解不防止纤维栈溢出不过它让某段代码能否安全地在 main context 上运行变得更容易判断。核心原语Async 包装器与 await 解包folly/fibers/async/Async.h是整套框架的基石定义了三个核心原语。Async 零成本、move-only 的返回值包装要声明一个函数可能阻塞因此必须在纤维上运行其返回类型必须标注为Async。Async是一个简单的零成本包装器必须通过一次await调用来解包获取结果。从源码 Async.h 可以看到关键设计template typename T class [[nodiscard]] Async { public: using inner_type T; // 通用构造任何可转发参数直接原地构造内部值 template typename... Us /* implicit */ Async(Us... val) : val_(std::forwardUs(val)...) {} // 移动构造允许不经过 await 的 eager-return类型可转换时 template typename U /* implicit */ Async(AsyncU async) noexcept : val_(static_castU(async.val_)) {} Async(const Async) delete; // 只允许移动 Async(Async other) default; Async operator(const Async) delete; Async operator(Async) delete; friend T tag_invoke(await_fn, Async async) noexcept { DCHECK(detail::onFiber()); // 调试期强制必须在纤维上下文 return static_castT(async.val_); } private: T val_; template typename U friend class Async; };几个值得注意的实现细节[[nodiscard]]返回值不允许被丢弃从编译期就要求调用者显式处理这个包装器防止遗漏解包导致结果悄悄丢失move-only拷贝构造与拷贝赋值被删除确保包装器只通过移动传递契合值仅由一次 await 消费的语义调试期DCHECK(detail::onFiber())await解包时若不在纤维上下文在 debug 构建中会直接断言失败——这就是文档所说运行时仅在 debug 构建中强制执行的实现载体。detail::onFiber()在 Async.cpp 中转发调用folly::fibers::onFiber()可转换移动构造AsyncU到AsyncT的隐式转换让Asyncstd::string可以直接转换为AsyncOptionalstd::string等测试 AsyncTest.cpp 验证了这一用法。Asyncvoid有专门特化Async.h可从Unit、AsyncUnit隐式构造其await不返回值、同样带DCHECK(detail::onFiber())。awaitCPO 化的解包操作await被实现为 folly 的定制点对象CPOawait_fnAsync.h通过tag_invoke机制与AsyncT::tag_invoke(await_fn, ...)连接。文档强调调用await的函数自身必须返回Async包装器否则包装就失去意义——而强制这一规则的最佳手段是静态分析lint。struct await_fn { template typename T auto operator()(AsyncT async) const noexcept(is_nothrow_tag_invocableawait_fn, AsyncT::value) - tag_invoke_result_tawait_fn, AsyncT { return tag_invoke(*this, static_castAsyncT(async)); } }; FOLLY_DEFINE_CPO(await_fn, await_async) static constexpr auto await await_async;配套类型萃取工具Async.h还提供了一组模板元编程工具供Collect.h、WaitUtils.h等上层 API 使用is_async_vT判断类型是否为Async的实例化async_inner_type_tAsyncT取出Async内部值类型Tasync_invocable_inner_type_tF, Args...求出可调用对象返回的AsyncT的内部类型T。init_await栈顶启动与渐进式迁移的钥匙init_awaitAsync.h与await行为一致内部就是调用await_async但不要求所在函数返回Asynctemplate typename T T init_await(AsyncT async) { return await_async(std::move(async)); } inline void init_await(Asyncvoid async) { await_async(std::move(async)); }它专门用于从栈顶开始注解例如提交给 fiber manager 的任务以及无法一次性迁移整个代码库时的渐进式落地。配合静态分析调用init_await的函数不应被注解这一规则可以反向保证迁移边界清晰。四类阻塞转换 API把阻塞操作变成 Async 调用文档 README.md 明确指出库为 fiber 中绝大多数阻塞操作提供了转换成Async返回函数调用的 API。下面逐一结合源码展开。promiseWait等待通用异步回调Promise.h 提供promiseWait包装底层fibers::await用于等待基于回调的通用异步接口template typename F Asynctypename FirstArgOfF::type::value_type promiseWait(F func) { return fibers::await_async(std::forwardF(func)); }其内部类型约定传入的回调F的第一个参数应是一个持有值的类型如PromiseTFirstArgOfF::type::value_type即取出其承载值类型T作为AsyncT的结果类型。baton_wait 系列等待 fibers::BatonBaton.h 提供三个封装逐一对应fibers::Baton的阻塞接口template typename... Args Asyncvoid baton_wait(Baton baton, Args... args) { baton.wait(std::forwardArgs(args)...); // 调用底层阻塞 API return {}; } template typename... Args Asyncbool baton_try_wait_for(Baton baton, Args... args) { return baton.try_wait_for(std::forwardArgs(args)...); // 超时版本返回 bool } template typename... Args Asyncbool baton_try_wait_until(Baton baton, Args... args) { return baton.try_wait_until(std::forwardArgs(args)...); // 截止时间版本 }注意baton_wait返回Asyncvoid而两个带超时/截止时间的版本返回Asyncbool等待是否成功参数Args...直接透传给底层Baton方法因此try_wait_for的时长、try_wait_until的时间点均可原样传入。测试 AsyncTest.cpp 验证了已 post 的 baton 立即可等待、超时等待按预期消耗时间等行为。futureWait等待 Future/SemiFuture 就绪Future.h 提供futureWait等待FutureT/SemiFutureT就绪并执行 deferred 工作template typename T AsyncT futureWait(SemiFutureT semi) { // Any deferred work will be executed inline on main-context return std::move(semi).get(); } template typename T AsyncT futureWait(FutureT fut) { return std::move(fut).get(); }SemiFuture版本会在 main-context 上内联执行其 deferred 工作源码注释明确说明这一语义因此它自身也必须以Async注解暴露会阻塞这一事实。taskWait阻塞等待 coro::TaskTask.h 提供taskWait在注解函数内阻塞等待一个folly::coro::TaskT完成template typename T AsyncT taskWait(folly::coro::TaskT task) { return folly::coro::blockingWait(std::move(task)); } inline Asyncvoid taskWait(folly::coro::Taskvoid task) { folly::coro::blockingWait(std::move(task)); return {}; }源码注释说明执行taskWait的纤维在 task 挂起期间阻塞task 的工作则在纤维的 main context 上内联执行。这一 API 正是纤维代码与 coro 代码桥接、并向 coro 渐进迁移的关键一环。扇出与调度collectAll 与 addFiber 系列collectAll并发执行多个注解函数folly/fibers/async/Collect.h实现见 Collect-inl.h提供三种形态的collectAll它们都接受返回Async的 functor作为输入迭代器版本Collect.hcollectAll(first, last)返回Asyncstd::vector...当所有任务都返回Asyncvoid时特化为返回Asyncvoid。内部通过detail::await_iteratorCollect-inl.h把输入迭代器包装为调用时自动init_await解包的 functor再交给底层folly::fibers::collectAll容器版本collectAll(Collection)直接转发为迭代器版本可变参数版本Collect-inl.hcollectAll(task1, task2, ...)返回Asyncstd::tuple...其中void返回值被提升为Unit。可变参数版本的实现collectAllImpl清晰地揭示了扇出的执行模型为除第一个之外的所有任务各addFiber一条新纤维当前纤维亲自执行第一个任务await_async(taskFunc(...))通过Baton与numPending计数器等待其余任务全部完成最后一个完成者b.post()唤醒汇总Try元组并解包返回。语义要点Collect.h 注释必须等所有任务完成后才统一返回若任一 functor 抛出异常会等全部任务完成后再重新抛出多个异常时只重抛其中一个。Collect.h还提供了配套工具awaitTry(F func)把注解函数的执行结果包进Try避免异常中断流程fromTry(folly::TryT)将Try转回AsyncTT为void时先throwUnlessValue()executeOnNewFiber(F func)在当前纤维上阻塞等待一个新纤维执行完注解函数。源码注释Collect.h提醒此 API 应谨慎使用其价值在于重置纤维栈的使用水位避免纤维栈溢出executeOnRemoteFiber(F func, FiberManager fm)从本地纤维阻塞等待远程线程纤维管理器上的执行结果。addFiber 系列把注解函数调度到 FiberManagerfolly/fibers/async/FiberManager.h包装了FiberManager的对应成员函数区别在于接受返回Async的可调用对象。实现上统一在包装 lambda 内部用init_await(func())解包template typename F void addFiber(F func, FiberManager fm) { fm.addTask([func std::forwardF(func)]() mutable { return init_await(func()); }); } template typename F Futurelift_unit_tasync_invocable_inner_type_tF addFiberFuture( F func, FiberManager fm) { return fm.addTaskFuture([func std::forwardF(func)]() mutable { return init_await(func()); }); }四个变体分别是addFiber/addFiberRemote本地/远程调度无返回值与addFiberFuture/addFiberRemoteFuture本地/远程调度返回Future。其中 Remote 变体把任务投递到另一个线程的 fiber manager 上执行。头文件注释FiberManager.h坦诚指出这些函数为了早期落地而公开但在设计原则上并不严格符合本库main context 与 fiber context 严格分离等长期目标未来应移入detail::——日常使用时官方建议优先选择executeOnNewFiber与executeOnFiberAndWait而非直接使用addFiberFuture。入口工具executeOnFiberAndWaitfolly/fibers/async/WaitUtils.h提供最常用的入口工具executeOnFiberAndWait在一个与EventBase关联的FiberManager上把注解函数运行到完成同时阻塞当前线程。它把创建 EventBase、获取 FiberManager、提交任务、getVia这一整套样板代码封装起来实现见 WaitUtils.hnamespace detail { template typename F lift_unit_tasync_invocable_inner_type_tF executeOnFiberAndWait( F func, folly::EventBase evb, FiberManager fm) { DCHECK(!detail::onFiber()); // 不允许在纤维上下文中调用 return addFiberFuture(std::forwardF(func), fm).getVia(evb); } } // namespace detail template typename F lift_unit_tasync_invocable_inner_type_tF executeOnFiberAndWait( F func, const FiberManager::Options opts FiberManager::Options()) { folly::EventBase evb; return detail::executeOnFiberAndWait( std::forwardF(func), evb, getFiberManager(evb, opts)); }公开了四组重载覆盖本地/外部EventBase×FiberManager::Options/FrozenOptions的组合FiberManager::Options可配置纤维栈大小等参数。另有executeOnRemoteFiberAndWait(F func, FiberManager fm)WaitUtils.h适用于库使用专用线程池运行纤维的场景通过addFiberRemoteFuture(...).get()阻塞当前线程。两者都带DCHECK(!detail::onFiber())明确禁止在纤维上下文中调用。基础示例从朴素 fibers到注解 fibers文档 README.md 给出了完全对照的两段示例先看朴素 fibers 代码using namespace folly; constexpr auto kWaitTime std::chrono::seconds{1}; auto blockingOperation1 [] { fibers::Baton b; b.try_wait_for(kWaitTime); }; auto blockingOperation2 [] { futures::sleep(kWaitTime).get(); }; auto blockingOperation3 [] { coro::blockingWait(coro::co_invoke( []() - coro::Taskvoid { co_await coro::sleep(kWaitTime); })); }; EventBase evb; fibers::getFiberManager(evb) .addTaskFuture([] { std::vectorFunctionvoid() tasks; tasks.emplace_back(blockingOperation1); tasks.emplace_back(blockingOperation2); tasks.emplace_back(blockingOperation3); fibers::collectAll(tasks.begin(), tasks.end()); }) .getVia(evb);这段代码的问题在于三个blockingOperation*的返回类型都是普通的void从签名上完全看不出它们会阻塞也无法保证它们跑在纤维上——阻塞行为被完全隐藏。再看注解后的 fibers 代码框架主推的写法using namespace folly; constexpr auto kWaitTime std::chrono::seconds{1}; auto blockingOperation1 []() - fibers::async::Asyncbool { fibers::Baton b; return fibers::async::baton_try_wait_for(b, kWaitTime); }; auto blockingOperation2 []() - fibers::async::AsyncUnit { return fibers::async::futureWait(futures::sleep(kWaitTime)); }; auto blockingOperation3 []() - fibers::async::AsyncUnit { return fibers::async::taskWait(coro::co_invoke([]() - coro::TaskUnit { co_await coro::sleep(kWaitTime); co_return{}; })); }; fibers::async::executeOnFiberAndWait([]() - fibers::async::Asyncvoid { fibers::async::await(fibers::async::collectAll( blockingOperation1, blockingOperation2, blockingOperation3)); return {}; });对照要点每个可能阻塞的 lambda 返回值被改为Asyncbool/AsyncUnit阻塞行为在类型层面一目了然Baton、Future、coro::Task三种阻塞源分别用baton_try_wait_for、futureWait、taskWait转换最外层用executeOnFiberAndWait作为进入纤维代码的入口在await(collectAll(...))中并发扇出三个操作与朴素版本collectAll语义等价但全部经Async注解整个栈从入口到叶子全部显式标注这正是整个代码库可注解的形态。注解整个代码库推荐工作流文档 README.md 给出了一套可操作的渐进式迁移步骤可保证所有可能阻塞的函数都被注解定位并翻译阻塞点识别代码中的阻塞操作如future.get()用库提供的 API 将其翻译为注解函数调用用await解包函数调用结果并将当前函数注解为返回Async引入静态分析 / lint强制任何调用await的函数自身必须返回Async——这是让注解不漏、不坏的机制保障用init_await降低单次改动面整个代码库难以一次迁移因此在当前改动中先用init_await代替await解包静态分析反向保证调用init_await的函数不应被注解使用扇出与调度 API用collectAllcollectFibers等扇出、用addFiber向FiberManager调度更多工作——这些 API 都要求传入注解过的 functor逐层替换init_await为await继续向调用栈更高层注解迁移结束时代码中不应残留任何init_await收尾用executeOnFiberAndWait作为进入纤维代码的入口确保连栈顶都被注解。这一工作流把全库注解拆解为可合并、可审查的增量改动与init_await的设计意图Async.h从栈顶开始注解的工具完全呼应。测试验证与进一步阅读库自带单元测试 AsyncTest.cpp覆盖了本文涉及的主要行为可作为注解代码的正确用法的活教材asyncAwait验证init_await解包字符串、可选值、元组、非拷贝非移动类型引用并用static_assert校验引用类型AsyncTest.cppasyncBaton验证baton_wait/baton_try_wait_for/baton_try_wait_until的就绪与超时路径AsyncTest.cpp。如需深入源码可按以下路径继续探索folly/fibers/async/Async.hAsync、await、init_await及类型萃取的核心定义folly/fibers/async/Collect.h 与 Collect-inl.hcollectAll、awaitTry、executeOnNewFiber的实现folly/fibers/async/WaitUtils.hexecuteOnFiberAndWait入口工具folly/fibers/async/Baton.h、Future.h、Promise.h、Task.h四类阻塞转换 APIfolly/fibers/async/test/AsyncTest.cpp行为验证用例。结语folly/fibers/async的价值不在于发明新的并发模型而在于用最小成本把 fibers 已有的隐式并发显式化Async是零开销的类型级标记await在 debug 构建下守护纤维上下文边界init_await支持渐进式全库注解collectAll与executeOnFiberAndWait则分别补齐了扇出与入口两块拼图。对于仍依赖folly/fibers的存量代码它既能立刻改善开发体验、消灭假并发类 bug又为最终迁往folly::coro铺平了道路——注解过的代码路径、已完成的 I/O 分离都是迁移时可以自动化的现成输入。【免费下载链接】follyAn open-source C library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/folly创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考