Folly Fibers Async 注解框架:用 Async<>/await 让纤维并发显式化
2026/9/10 8:24:00 网站建设 项目流程

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/awaitfolly::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::forward<Us>(val)...) {} // 移动构造:允许不经过 await 的 eager-return(类型可转换时) template <typename U> /* implicit */ Async(Async<U>&& async) noexcept : val_(static_cast<U&&>(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_cast<T&&>(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()
  • 可转换移动构造Async<U>&&Async<T>&&的隐式转换,让Async<std::string>可以直接转换为Async<Optional<std::string>>等,测试 AsyncTest.cpp 验证了这一用法。

Async<void>有专门特化(Async.h):可从UnitAsync<Unit>隐式构造,其await不返回值、同样带DCHECK(detail::onFiber())

await:CPO 化的解包操作

await被实现为 folly 的定制点对象(CPO)await_fn(Async.h),通过tag_invoke机制与Async<T>::tag_invoke(await_fn, ...)连接。文档强调:调用await的函数自身必须返回Async<>包装器,否则包装就失去意义——而强制这一规则的最佳手段是静态分析(lint)。

struct await_fn { template <typename T> auto operator()(Async<T>&& async) const noexcept(is_nothrow_tag_invocable<await_fn, Async<T>&&>::value) -> tag_invoke_result_t<await_fn, Async<T>&&> { return tag_invoke(*this, static_cast<Async<T>&&>(async)); } }; FOLLY_DEFINE_CPO(await_fn, await_async) static constexpr auto& await = await_async;

配套类型萃取工具

Async.h还提供了一组模板元编程工具,供Collect.hWaitUtils.h等上层 API 使用:

  • is_async_v<T>:判断类型是否为Async的实例化;
  • async_inner_type_t<Async<T>>:取出Async<>内部值类型T
  • async_invocable_inner_type_t<F, Args...>:求出可调用对象返回的Async<T>的内部类型T

init_await:栈顶启动与渐进式迁移的钥匙

init_await(Async.h)与await行为一致(内部就是调用await_async),但不要求所在函数返回Async<>

template <typename T> T&& init_await(Async<T>&& async) { return await_async(std::move(async)); } inline void init_await(Async<void>&& 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> Async<typename FirstArgOf<F>::type::value_type> promiseWait(F&& func) { return fibers::await_async(std::forward<F>(func)); }

其内部类型约定:传入的回调F的第一个参数应是一个持有值的类型(如Promise<T>),FirstArgOf<F>::type::value_type即取出其承载值类型T,作为Async<T>的结果类型。

baton_wait 系列:等待 fibers::Baton

Baton.h 提供三个封装,逐一对应fibers::Baton的阻塞接口:

template <typename... Args> Async<void> baton_wait(Baton& baton, Args&&... args) { baton.wait(std::forward<Args>(args)...); // 调用底层阻塞 API return {}; } template <typename... Args> Async<bool> baton_try_wait_for(Baton& baton, Args&&... args) { return baton.try_wait_for(std::forward<Args>(args)...); // 超时版本,返回 bool } template <typename... Args> Async<bool> baton_try_wait_until(Baton& baton, Args&&... args) { return baton.try_wait_until(std::forward<Args>(args)...); // 截止时间版本 }

注意baton_wait返回Async<void>,而两个带超时/截止时间的版本返回Async<bool>(等待是否成功),参数Args&&...直接透传给底层Baton方法,因此try_wait_for的时长、try_wait_until的时间点均可原样传入。测试 AsyncTest.cpp 验证了已 post 的 baton 立即可等待、超时等待按预期消耗时间等行为。

futureWait:等待 Future/SemiFuture 就绪

Future.h 提供futureWait,等待Future<T>/SemiFuture<T>就绪并执行 deferred 工作:

template <typename T> Async<T> futureWait(SemiFuture<T>&& semi) { // Any deferred work will be executed inline on main-context return std::move(semi).get(); } template <typename T> Async<T> futureWait(Future<T>&& fut) { return std::move(fut).get(); }

SemiFuture版本会在 main-context 上内联执行其 deferred 工作(源码注释明确说明这一语义),因此它自身也必须以Async<>注解暴露"会阻塞"这一事实。

taskWait:阻塞等待 coro::Task

Task.h 提供taskWait,在注解函数内阻塞等待一个folly::coro::Task<T>完成:

template <typename T> Async<T> taskWait(folly::coro::Task<T>&& task) { return folly::coro::blockingWait(std::move(task)); } inline Async<void> taskWait(folly::coro::Task<void>&& 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.h):collectAll(first, last),返回Async<std::vector<...>>;当所有任务都返回Async<void>时特化为返回Async<void>。内部通过detail::await_iterator(Collect-inl.h)把输入迭代器包装为"调用时自动init_await解包"的 functor,再交给底层folly::fibers::collectAll
  • 容器版本collectAll(Collection&&)直接转发为迭代器版本;
  • 可变参数版本(Collect-inl.h):collectAll(task1, task2, ...),返回Async<std::tuple<...>>,其中void返回值被提升为Unit

可变参数版本的实现(collectAllImpl)清晰地揭示了"扇出"的执行模型:

  1. 为除第一个之外的所有任务各addFiber一条新纤维;
  2. 当前纤维亲自执行第一个任务(await_async(taskFunc(...)));
  3. 通过BatonnumPending计数器等待其余任务全部完成(最后一个完成者b.post()唤醒);
  4. 汇总Try元组并解包返回。

语义要点(Collect.h 注释):必须等所有任务完成后才统一返回;若任一 functor 抛出异常,会等全部任务完成后再重新抛出(多个异常时只重抛其中一个)。

Collect.h还提供了配套工具:

  • awaitTry(F&& func):把注解函数的执行结果包进Try,避免异常中断流程;
  • fromTry(folly::Try<T>&&):将Try转回Async<T>Tvoid时先throwUnlessValue());
  • executeOnNewFiber(F&& func):在当前纤维上阻塞等待一个新纤维执行完注解函数。源码注释(Collect.h)提醒:此 API 应谨慎使用,其价值在于重置纤维栈的使用水位,避免纤维栈溢出;
  • executeOnRemoteFiber(F&& func, FiberManager& fm):从本地纤维阻塞等待远程线程纤维管理器上的执行结果。

addFiber 系列:把注解函数调度到 FiberManager

folly/fibers/async/FiberManager.h包装了FiberManager的对应成员函数,区别在于接受返回Async<>的可调用对象。实现上统一在包装 lambda 内部用init_await(func())解包:

template <typename F> void addFiber(F&& func, FiberManager& fm) { fm.addTask([func = std::forward<F>(func)]() mutable { return init_await(func()); }); } template <typename F> Future<lift_unit_t<async_invocable_inner_type_t<F>>> addFiberFuture( F&& func, FiberManager& fm) { return fm.addTaskFuture([func = std::forward<F>(func)]() mutable { return init_await(func()); }); }

四个变体分别是:addFiber/addFiberRemote(本地/远程调度,无返回值)与addFiberFuture/addFiberRemoteFuture(本地/远程调度,返回Future)。其中 "Remote" 变体把任务投递到另一个线程的 fiber manager 上执行。头文件注释(FiberManager.h)坦诚指出:这些函数为了早期落地而公开,但在设计原则上并不严格符合本库"main context 与 fiber context 严格分离"等长期目标,未来应移入detail::——日常使用时,官方建议优先选择executeOnNewFiberexecuteOnFiberAndWait而非直接使用addFiberFuture

入口工具:executeOnFiberAndWait

folly/fibers/async/WaitUtils.h提供最常用的入口工具executeOnFiberAndWait:在一个与EventBase关联的FiberManager上把注解函数运行到完成,同时阻塞当前线程。它把"创建 EventBase、获取 FiberManager、提交任务、getVia"这一整套样板代码封装起来(实现见 WaitUtils.h):

namespace detail { template <typename F> lift_unit_t<async_invocable_inner_type_t<F>> executeOnFiberAndWait( F&& func, folly::EventBase& evb, FiberManager& fm) { DCHECK(!detail::onFiber()); // 不允许在纤维上下文中调用 return addFiberFuture(std::forward<F>(func), fm).getVia(&evb); } } // namespace detail template <typename F> lift_unit_t<async_invocable_inner_type_t<F>> executeOnFiberAndWait( F&& func, const FiberManager::Options& opts = FiberManager::Options()) { folly::EventBase evb; return detail::executeOnFiberAndWait( std::forward<F>(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::Task<void> { co_await coro::sleep(kWaitTime); })); }; EventBase evb; fibers::getFiberManager(evb) .addTaskFuture([&] { std::vector<Function<void()>> 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::Async<bool> { fibers::Baton b; return fibers::async::baton_try_wait_for(b, kWaitTime); }; auto blockingOperation2 = [&]() -> fibers::async::Async<Unit> { return fibers::async::futureWait(futures::sleep(kWaitTime)); }; auto blockingOperation3 = [&]() -> fibers::async::Async<Unit> { return fibers::async::taskWait(coro::co_invoke([&]() -> coro::Task<Unit> { co_await coro::sleep(kWaitTime); co_return{}; })); }; fibers::async::executeOnFiberAndWait([&]() -> fibers::async::Async<void> { fibers::async::await(fibers::async::collectAll( blockingOperation1, blockingOperation2, blockingOperation3)); return {}; });

对照要点:

  • 每个可能阻塞的 lambda 返回值被改为Async<bool>/Async<Unit>,阻塞行为在类型层面一目了然
  • BatonFuturecoro::Task三种阻塞源分别用baton_try_wait_forfutureWaittaskWait转换;
  • 最外层用executeOnFiberAndWait作为进入纤维代码的入口,在await(collectAll(...))中并发扇出三个操作(与朴素版本collectAll语义等价,但全部经Async注解);
  • 整个栈从入口到叶子全部显式标注,这正是"整个代码库可注解"的形态。

注解整个代码库:推荐工作流

文档 README.md 给出了一套可操作的渐进式迁移步骤,可保证所有可能阻塞的函数都被注解

  1. 定位并翻译阻塞点:识别代码中的阻塞操作(如future.get()),用库提供的 API 将其翻译为注解函数调用;用await解包函数调用结果,并将当前函数注解为返回Async<>
  2. 引入静态分析 / lint:强制任何调用await的函数自身必须返回Async<>——这是让注解"不漏、不坏"的机制保障;
  3. init_await降低单次改动面:整个代码库难以一次迁移,因此在当前改动中先用init_await代替await解包;静态分析反向保证"调用init_await的函数不应被注解";
  4. 使用扇出与调度 API:用collectAllcollectFibers等)扇出、用addFiberFiberManager调度更多工作——这些 API 都要求传入注解过的 functor;
  5. 逐层替换init_awaitawait:继续向调用栈更高层注解;迁移结束时,代码中不应残留任何init_await
  6. 收尾:用executeOnFiberAndWait作为进入纤维代码的入口,确保连栈顶都被注解

这一工作流把"全库注解"拆解为可合并、可审查的增量改动,与init_await的设计意图(Async.h:"从栈顶开始注解的工具")完全呼应。

测试验证与进一步阅读

库自带单元测试 AsyncTest.cpp,覆盖了本文涉及的主要行为,可作为"注解代码的正确用法"的活教材:

  • asyncAwait:验证init_await解包字符串、可选值、元组、非拷贝非移动类型引用,并用static_assert校验引用类型(AsyncTest.cpp);
  • asyncBaton:验证baton_wait/baton_try_wait_for/baton_try_wait_until的就绪与超时路径(AsyncTest.cpp)。

如需深入源码,可按以下路径继续探索:

  • folly/fibers/async/Async.h:Async<>awaitinit_await及类型萃取的核心定义;
  • folly/fibers/async/Collect.h 与 Collect-inl.h:collectAllawaitTryexecuteOnNewFiber的实现;
  • folly/fibers/async/WaitUtils.h:executeOnFiberAndWait入口工具;
  • folly/fibers/async/Baton.h、Future.h、Promise.h、Task.h:四类阻塞转换 API;
  • folly/fibers/async/test/AsyncTest.cpp:行为验证用例。

结语

folly/fibers/async的价值不在于发明新的并发模型,而在于用最小成本把 fibers 已有的隐式并发显式化Async<>是零开销的类型级标记,await在 debug 构建下守护纤维上下文边界,init_await支持渐进式全库注解,collectAllexecuteOnFiberAndWait则分别补齐了扇出与入口两块拼图。对于仍依赖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),仅供参考

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

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

立即咨询