- 语言运行时
- 编译器
- 移动开发
【免费下载链接】hermes
A JavaScript engine optimized for running React Native.
导读
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 需要C++11(README 末尾明确注明 "boost.context requires C++11!")。本仓库内置的 boost 1.86.0 即满足该要求。
二、核心 API:fiber 的创建、挂起与恢复
虽然 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); }); // 主路径启动/恢复 fiber(resume 返回控制权并传入新的 fiber 对象) f = std::move(f).resume();几个关键设计点:
resume()的双向语义:主路径调用resume()进入 fiber,fiber 内部再次调用resume()则把控制权交还主路径,且resume()的返回值就是"对方"传入的 fiber 对象,因此必须用std::move保存,否则当前 fiber 会因持有悬空引用而出错——这正是源码注释 "The std::move() here is required by older compilers" 强调的细节;- 所有权随控制权转移:fiber 对象本身是移动语义对象,控制权在谁手里,谁就持有对应的
fiber实例; - 终止语义:当 fiber 的可调用对象返回而不是再次
resume()时,fiber 执行结束。
2.2 栈分配器与 stack_traits
fiber 需要自己的栈。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_local
3.1 三层汇编函数:make / jump / ontop
fcontext 实现(默认实现,对应 CMake 中的BOOST_CONTEXT_IMPLEMENTATION=fcontext)的核心是三个汇编函数,每个都针对「架构 × 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.txt):
set(_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_ARCHITECTURE | x86_64、arm64、riscv64、s390x等 | 目标架构 |
BOOST_CONTEXT_ABI | sysv、aapcs、ms、o32/n64等 | 二进制调用约定 |
BOOST_CONTEXT_BINARY_FORMAT | elf、mach-o、pe、xcoff | 目标文件格式 |
BOOST_CONTEXT_ASSEMBLER | gas、masm、armasm | 汇编器类型 |
其中 ABI 的推导逻辑(CMakeLists.txt)是:ARM 系用aapcs,Windows 用ms,MIPS 按 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 record:thread_local 的"当前上下文"登记
fiber的 C++ 侧封装维护了一个每线程的 activation record。见 src/fiber.cpp:
namespace 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()惰性初始化自己的 record;- Schwarz 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_IMPLEMENTATION=ucontext|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.cpp):
class 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 单线程环境没有独立的执行栈概念;- 其余情况:退化为基于
SerialExecutor+std::packaged_task的线程池实现,用 OS 线程串行执行回调。
这种三态设计说明:boost.context 的 fiber 是 Hermes 在"需要独立执行栈但不想付出线程切换成本"场景下的首选实现,其他平台则按能力降级。
4.3 单元测试
boost.context 自带的测试位于 libs/context/test,包括:
test_fiber.cpp:fiber API 的挂起/恢复/终止行为测试;test_callcc.cpp:continuation 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 中的真实落地,都是极具参考价值的范本。
- 语言运行时
- 编译器
- 移动开发
【免费下载链接】hermes
A JavaScript engine optimized for running React Native.
相关推荐
Nuclide工作集自动切换:基于项目目录的上下文
Nuclide工作集自动切换:基于项目目录的上下文 你是否曾在大型项目中迷失于繁杂的文件结构?频繁切换项目时,是否希望IDE能智能展示当前任务所需的文件?Nuc
开发工具如何快速部署Qwen2.5-7B-Instruct-GPTQ-Int4:5分钟完成本地AI助手搭建
如何快速部署Qwen2.5 7B Instruct GPTQ Int4:5分钟完成本地AI助手搭建 想要在本地快速搭建一个强大的AI助手吗?Qwen2.5 7B
dotnet-starter-kit 后台任务实战:基于 Hangfire 的 IJobService、定时任务与多租户上下文还原
dotnet starter kit 后台任务实战:基于 Hangfire 的 IJobService、定时任务与多租户上下文还原 导读 在 FSH.Start
后端前端示例工程认证鉴权
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考