Hermes 中的 boost.context:基于 fiber 的协作式多任务上下文切换原理与集成实践
语言运行时编译器移动开发【免费下载链接】hermesA JavaScript engine optimized for running React Native.项目地址https://gitcode.com/gh_mirrors/hermes/hermes点击查看免费下载导读boost.context 是一个为 C 提供「单线程内协作式多任务」能力的基础库它通过抽象当前线程的执行状态栈、栈指针、寄存器、CPU 标志与指令指针来构建协程、用户态线程fiber等高级抽象。本文以 Hermes 仓库内置的 boost.context README 为核心结合其汇编级源码与 Hermes 的 StackExecutor 集成实现讲解 execution_context 抽象、fiber 挂起/恢复机制、上下文切换的性能优势以及 Hermes 如何用 fiber API 在自己的执行栈上运行 JS 回调。一、什么是 boost.context单线程上的协作式多任务boost.context 是 Boost 库族中的基础库foundational library其定位非常明确在一个线程上提供一种协作式多任务cooperative multitasking的机制。它自身不实现协程、不实现调度器而是向更上层提供「保存与恢复执行上下文」的原子能力上层可以据此自由构建协程coroutines协作式线程cooperative threads即用户态线程/userland threads类似 C# 中yield关键字在 C 中的等价物。这一点从库的源码组织上也能印证仓库中的libs/context/src目录只包含fiber.cpp、continuation.cpp、dummy.cpp、untested.cpp及平台相关的posix/stack_traits.cpp、windows/stack_traits.cpp本身并不包含任何调度器或协程语义全部是底层执行上下文的搬运逻辑见 libs/context/src 目录。1.1 execution_context执行路径上的一帧快照README 中给出了该库的核心数据模型通过抽象当前线程中的当前执行状态——包括栈含局部变量和栈指针、所有寄存器与 CPU 标志、指令指针——一个execution_context实例就代表了应用程序执行路径上的一个特定点。换句话说一次上下文切换就是对上述全部状态的一次整体保存与恢复。由于保存的是完整的寄存器集与栈内容从被保存点恢复后程序可以无缝继续执行仿佛从未离开过。boost::context::continuation与boost::context::fiber都是这一抽象在 API 层的具体呈现。1.2 fiber挂起与转移执行控制README 对 fiber 的定义是A fiber 提供了挂起当前执行路径并把执行控制转移出去的手段从而允许另一个 fiber 在当前线程上运行。这种带状态的转移机制使一个 fiber 能够从嵌套函数内部挂起并在之后从挂起点恢复执行。关键特性可拆解为三点任意深度挂起挂起点不必是顶层函数fiber 可以从任意嵌套调用栈深处挂起精确恢复恢复时从挂起点继续局部变量、栈帧完好无损线程可迁移虽然一个 fiber 表示的执行路径某一时刻只在一个线程上运行但它可以在任意时刻被迁移到另一个线程源码实现层面由thread_local的 activation record 机制保证。1.3 为什么需要它一次上下文切换的成本对比README 给出了一个非常直观的性能依据切换类型是否涉及系统调用x86 上的成本量级线程间切换是需进入 OS 内核超过 1000 个 CPU 周期fiber 间控制转移否单线程内完成少于 100 个 CPU 周期之所以有近一个数量级的差距是因为线程切换要经过内核调度而 fiber 切换是纯用户态操作只需要保存/恢复寄存器与栈指针然后跳转即可完全不触碰内核。版本要求boost.context 需要C11README 末尾明确注明 boost.context requires C11!。本仓库内置的 boost 1.86.0 即满足该要求。二、核心 APIfiber 的创建、挂起与恢复虽然 README 是概念性的但从源码可以确认它的两大 API 族continuation与fiber。而 Hermes 仓库在 libs/context/CMakeLists.txt 中明确做了裁剪else() # Hermes only uses the fiber API, not the continuation API. # Skip continuation.cpp since continuation_ucontext.hpp is not included. set(IMPL_SOURCES src/fiber.cpp ) endif()也就是说Hermes 只编译src/fiber.cpp跳过continuation.cpp。因此在本文语境下我们重点讨论fiber API。2.1 fiber 的典型生命周期fiber API 的核心对象是boost::context::fiber其基本用法模式与 Hermes 的 StackExecutor.cpp 中的用法一致为#include boost/context/fiber.hpp #include boost/context/protected_fixedsize_stack.hpp namespace ctx boost::context; // 创建一个 fiber分配独立栈入口是一个接收 ctx::fiber 的可调用对象 ctx::fiber f(std::allocator_arg, ctx::protected_fixedsize_stack(128 * 1024), [](ctx::fiber next) { // ... 用户代码 ... // 把控制权交还调用者 next std::move(next).resume(); return std::move(next); }); // 主路径启动/恢复 fiberresume 返回控制权并传入新的 fiber 对象 f std::move(f).resume();几个关键设计点resume()的双向语义主路径调用resume()进入 fiberfiber 内部再次调用resume()则把控制权交还主路径且resume()的返回值就是对方传入的 fiber 对象因此必须用std::move保存否则当前 fiber 会因持有悬空引用而出错——这正是源码注释 The std::move() here is required by older compilers 强调的细节所有权随控制权转移fiber 对象本身是移动语义对象控制权在谁手里谁就持有对应的fiber实例终止语义当 fiber 的可调用对象返回而不是再次resume()时fiber 执行结束。2.2 栈分配器与 stack_traitsfiber 需要自己的栈。boost.context 提供多种栈分配器Hermes 使用的是protected_fixedsize_stack带保护页的固定大小栈它通过在栈底放置不可读写的保护页来检测栈溢出。配套的boost::context::stack_traits提供栈尺寸查询POSIX 实现见 src/posix/stack_traits.cpp其行为可以总结为方法POSIX 实现值说明default_size()128 × 1024 字节128 KiB未指定栈大小时使用的默认值minimum_size()MINSIGSTKSZ未定义时为 131072 字节最小允许栈尺寸maximum_size()getrlimit(RLIMIT_STACK)的rlim_max系统栈上限page_size()sysconf(_SC_PAGESIZE)系统页大小is_unbounded()RLIM_INFINITY limit栈上限是否无界Hermes 的 StackExecutor.cpp 正是这样使用它们的当未显式传入栈大小时回退到ctx::stack_traits::default_size()即 128 KiB。三、源码级原理汇编切换、activation record 与 thread_local3.1 三层汇编函数make / jump / ontopfcontext 实现默认实现对应 CMake 中的BOOST_CONTEXT_IMPLEMENTATIONfcontext的核心是三个汇编函数每个都针对「架构 × ABI × 二进制格式 × 汇编器」提供专属版本见 src/asm 目录make_*.S创建新的执行上下文——在新栈上布置初始栈帧填入入口函数地址返回一个可被 jump 的 context 指针jump_*.S保存当前上下文寄存器、栈指针等切换到目标上下文即完成一次交出/拿回控制权ontop_*.S在目标上下文之上执行一个额外函数后再继续用于实现上层语义如向正在运行的协程注入结果。以jump_x86_64_sysv_elf_gas.S、make_arm64_aapcs_elf_gas.S等文件为代表命名规范是{make|jump|ontop}_{架构}_{ABI}_{二进制格式}_{汇编器}.{S|asm}。CMake 侧通过四个缓存变量组合出要编译的汇编文件见 CMakeLists.txtset(_asm_suffix ${BOOST_CONTEXT_ARCHITECTURE}_${BOOST_CONTEXT_ABI}_${BOOST_CONTEXT_BINARY_FORMAT}_${BOOST_CONTEXT_ASSEMBLER}${BOOST_CONTEXT_ASM_SUFFIX}) set(ASM_SOURCES src/asm/make_${_asm_suffix} src/asm/jump_${_asm_suffix} src/asm/ontop_${_asm_suffix} )由 CMake 探测自动推导的四个变量分别是变量取值示例含义BOOST_CONTEXT_ARCHITECTUREx86_64、arm64、riscv64、s390x等目标架构BOOST_CONTEXT_ABIsysv、aapcs、ms、o32/n64等二进制调用约定BOOST_CONTEXT_BINARY_FORMATelf、mach-o、pe、xcoff目标文件格式BOOST_CONTEXT_ASSEMBLERgas、masm、armasm汇编器类型其中 ABI 的推导逻辑CMakeLists.txt是ARM 系用aapcsWindows 用msMIPS 按 32/64 位选o32/n64其余默认sysv。此外 Apple 平台比较特殊直接使用三个平台无关文件make_apple.S、jump_apple.S、ontop_apple.S由预处理宏在编译期决定目标 CPU注释中提到这允许clang -arch x86_64 -arch arm64一次编译出双架构产物。3.2 activation recordthread_local 的当前上下文登记fiber的 C 侧封装维护了一个每线程的 activation record。见 src/fiber.cppnamespace boost { namespace context { namespace detail { // zero-initialization thread_local fiber_activation_record * fib_current_rec; thread_local static std::size_t counter; // schwarz counter fiber_activation_record_initializer::fiber_activation_record_initializer() noexcept { if ( 0 counter) { fib_current_rec new fiber_activation_record(); } } ... fiber_activation_record * fiber_activation_record::current() noexcept { // initialized the first time control passes; per thread thread_local static fiber_activation_record_initializer initializer; return fib_current_rec; } }}}这段代码透露了几个关键机制thread_local存储每个线程各自持有fib_current_rec这正是 README 所说 fiber 可在任意时刻迁移到另一线程 的实现基础——切换线程后新线程通过current()惰性初始化自己的 recordSchwarz Counter 惯用法用静态计数器保证fib_current_rec只被构造/销毁一次且按依赖顺序正确析构析构时断言当前仍处于主上下文见BOOST_ASSERT(current_rec-is_main_context())惰性初始化current()首次被调用时才通过局部thread_local static触发初始化器避免全局构造开销。continuation.cpp中activation_record::current()的写法与此完全对称只是 record 类型不同见 continuation.cpp。另外值得注意的是fiber.cpp通过BOOST_USE_UCONTEXT/BOOST_USE_WINFIB宏可以切换到基于 POSIXucontext或 Windows Fiber API 的替代实现对应 CMake 的BOOST_CONTEXT_IMPLEMENTATIONucontext|winfib但默认的fcontext实现完全不依赖操作系统 API这正是其高性能的来源。四、Hermes 中的实际集成StackExecutor 与保护栈4.1 集成方式Hermes 在 lib/Support/StackExecutor.cpp 中把 boost.context 的 fiber 封装为StackExecutor为一段 JS 原生回调分配独立的 128 KiB 保护栈并在该栈上执行用户函数。启用条件是编译宏HERMES_USE_BOOST_CONTEXT见文件第 10 行#if defined(HERMES_USE_BOOST_CONTEXT) HERMES_USE_BOOST_CONTEXT。核心实现逻辑StackExecutor.cppclass StackExecutor { void *arg_ nullptr; void (*fn_)(void *) nullptr; // 每次 fiber 恢复时执行的函数为 nullptr 则 fiber 终止 ctx::fiber fiber_; public: explicit StackExecutor(size_t stackSize 0) : fiber_(std::allocator_arg, ctx::protected_fixedsize_stack( stackSize ? stackSize : ctx::stack_traits::default_size()), this { while (fn_) { // 循环直到 callee 变为 nullptr (*fn_)(arg_); // 在独立栈上执行 next std::move(next).resume(); // 交还控制权 } return std::move(next); // 终止 fiber }) {} void exec(void *arg, void (*fn)(void *arg)) { assert(fn_ nullptr StackExecutor recursive call??); arg_ arg; fn_ fn; fiber_ std::move(fiber_).resume(); // 进入/恢复 fiber arg_ nullptr; fn_ nullptr; } ~StackExecutor() { // 通过 resume 让 fiber 观察到 fn_ nullptr 而退出 fiber_ std::move(fiber_).resume(); } };这段代码与 README 描述的 fiber 语义一一对应fiber 能从嵌套函数挂起(*fn_)(arg_)内部可以任意调用更深层函数只要最终回到这里调用resume()控制权就能完整交还带状态的转移机制exec()每次调用把fn_/arg_换成新值后再resume()fiber 循环会以新参数重新执行而栈上的一切局部状态保持连续从挂起点恢复多次exec()之间fiber 的栈帧原封不动仅入口函数指针被替换。同时它还演示了protected_fixedsize_stack的价值Hermes 用它在一个独立、带保护页的栈上运行第三方/JS 侧回调一旦栈溢出会立即触发保护页错误而不是静默破坏内存。4.2 平台回退有意思的是StackExecutor.cpp 提供了三套实现从源码结构上可以推断其设计意图启用HERMES_USE_BOOST_CONTEXT时使用上述 fiber 实现单线程内完成切换性能最优__wasm__且无线程支持时退化为 no-op直接内联调用带编译警告因为 wasm 单线程环境没有独立的执行栈概念其余情况退化为基于SerialExecutorstd::packaged_task的线程池实现用 OS 线程串行执行回调。这种三态设计说明boost.context 的 fiber 是 Hermes 在需要独立执行栈但不想付出线程切换成本场景下的首选实现其他平台则按能力降级。4.3 单元测试boost.context 自带的测试位于 libs/context/test包括test_fiber.cppfiber API 的挂起/恢复/终止行为测试test_callcc.cppcontinuation API 的测试test_fcontext.cpp底层 fcontext 切换原语的测试test_apply.cpp/test_invoke.cpp上下文调用约定的测试。这些测试与上文所述 API 一一对应是理解每个 API 语义最终落在哪个汇编原语上的很好入口。五、总结boost.context 为 C 生态提供了一个教科书级的执行上下文抽象概念层execution_context保存栈 栈指针 寄存器 CPU 标志 指令指针这一整帧状态fiber 借此实现单线程内、任意嵌套深度的挂起与恢复性能层fiber 切换是纯用户态操作不经过内核x86 上开销少于 100 个 CPU 周期而线程切换因系统调用通常超过 1000 个周期实现层默认fcontext实现由 make/jump/ontop 三组手写汇编完成寄存器与栈的搬运C 侧以thread_localactivation record Schwarz Counter 管理每线程的当前上下文并通过stack_traits与多种栈分配器如protected_fixedsize_stack管理栈生命周期工程层Hermes 将其裁剪为只使用 fiber API见 CMakeLists.txt在 StackExecutor.cpp 中用它实现在独立保护栈上执行回调并配套了 wasm / 线程池两套平台回退方案。对于希望在 C 中实现协程、用户态线程或yield语义的开发者boost.context 的这套基础能力 可裁剪集成设计以及在 Hermes 中的真实落地都是极具参考价值的范本。赞分享语言运行时编译器移动开发【免费下载链接】hermesA JavaScript engine optimized for running React Native.项目地址https://gitcode.com/gh_mirrors/hermes/hermes点击查看免费下载相关推荐Nuclide工作集自动切换基于项目目录的上下文Nuclide工作集自动切换基于项目目录的上下文 你是否曾在大型项目中迷失于繁杂的文件结构频繁切换项目时是否希望IDE能智能展示当前任务所需的文件Nuc开发工具如何快速部署Qwen2.5-7B-Instruct-GPTQ-Int45分钟完成本地AI助手搭建如何快速部署Qwen2.5 7B Instruct GPTQ Int45分钟完成本地AI助手搭建 想要在本地快速搭建一个强大的AI助手吗Qwen2.5 7Bdotnet-starter-kit 后台任务实战基于 Hangfire 的 IJobService、定时任务与多租户上下文还原dotnet starter kit 后台任务实战基于 Hangfire 的 IJobService、定时任务与多租户上下文还原 导读 在 FSH.Start后端前端示例工程认证鉴权创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考