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::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):可从Unit、Async<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.h、WaitUtils.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)清晰地揭示了"扇出"的执行模型:
- 为除第一个之外的所有任务各
addFiber一条新纤维; - 当前纤维亲自执行第一个任务(
await_async(taskFunc(...))); - 通过
Baton与numPending计数器等待其余任务全部完成(最后一个完成者b.post()唤醒); - 汇总
Try元组并解包返回。
语义要点(Collect.h 注释):必须等所有任务完成后才统一返回;若任一 functor 抛出异常,会等全部任务完成后再重新抛出(多个异常时只重抛其中一个)。
Collect.h还提供了配套工具:
awaitTry(F&& func):把注解函数的执行结果包进Try,避免异常中断流程;fromTry(folly::Try<T>&&):将Try转回Async<T>(T为void时先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::——日常使用时,官方建议优先选择executeOnNewFiber与executeOnFiberAndWait而非直接使用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>,阻塞行为在类型层面一目了然; 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:用
collectAll(collectFibers等)扇出、用addFiber向FiberManager调度更多工作——这些 API 都要求传入注解过的 functor; - 逐层替换
init_await为await:继续向调用栈更高层注解;迁移结束时,代码中不应残留任何init_await; - 收尾:用
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<>、await、init_await及类型萃取的核心定义; - folly/fibers/async/Collect.h 与 Collect-inl.h:
collectAll、awaitTry、executeOnNewFiber的实现; - 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支持渐进式全库注解,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),仅供参考