协程挂起时只保存最小必要状态:await_suspend返回的handle、跨挂起点的局部变量(含引用和lambda捕获)、挂起点前的栈上临时对象;不保存TLS、全局变量、寄存器及上层函数局部变量。

协程挂起时到底保存了什么
协程挂起不是“暂停整个线程”,而是把当前函数执行现场的**最小必要状态**快照存起来,等恢复时能接着往下跑。C++20 协程标准不规定具体保存哪些寄存器或栈帧,但所有编译器(MSVC / Clang / GCC)都必须保存以下三类信息:
-
await_suspend返回的handle:这是恢复协程的唯一入口,必须保存 - 局部变量(含引用、
const修饰符、lambda 捕获):只要生命周期跨挂起点,就进协程帧(coroutine frame) - 挂起点前的栈上临时对象:比如
std::string s = "hello"在co_await前构造,它必须活到恢复后——所以会被移到协程帧里
不会保存的东西:线程局部存储(TLS)、全局变量、寄存器(如 rax)、调用栈中更上层函数的局部变量(除非它们被当前协程闭包捕获)。
为什么有些变量没进协程帧却还能用
常见错觉:“我啥都没动,变量还在那儿”。其实是编译器做了优化:
- 纯右值临时量(如
co_await std::make_ready_future(42)中的42)可能被直接常量折叠,不占空间 - 未跨挂起点的局部变量(比如定义在
co_await后面的int x = 1;)根本不会被保存——恢复后重新执行初始化语句 - 引用类型若绑定到静态/全局对象(如
int& r = global_i;),只存地址,不复制值
真正危险的是绑定到栈变量的引用:int local = 1; int& r = local; —— 若 local 所在栈帧已退出(比如挂起发生在函数返回前),恢复后 r 就是悬垂引用。
立即学习“C++免费学习笔记(深入)”;
怎么查自己协程帧里实际存了啥
别猜,直接看编译器生成的帧结构。Clang 和 GCC 支持 -fsanitize=undefined + -g,配合 gdb 查看协程帧地址;更直接的是用编译器内置宏和调试信息:
- 加
-fcoroutines -g编译,用objdump -t或readelf -s查找以__coro开头的符号,对应帧布局 - 在
promise_type::get_return_object里打印sizeof(*this)(注意:这是 promise 对象大小,不是整个帧) - 用
clang++ -Xclang -emit-coroutine-expansions可看到协程被重写成状态机后的代码,里面struct成员就是实际保存的字段
MSVC 下可用 /d1reportAllClassLayout 输出类布局,找到你的 promise_type 和编译器合成的帧 struct。
挂起后栈指针和寄存器谁负责恢复
协程切换不涉及传统上下文切换(比如 setjmp/longjmp 或系统级 swapcontext)。C++ 协程的“恢复”本质是:调用之前保存的 coroutine_handle<> 的 resume(),它会跳转回协程函数内某处(通常是挂起点之后的恢复点),并从协程帧里重新加载局部变量值。
- 栈指针(
rsp/sp)由编译器生成的恢复代码重置——它把协程帧里的变量“放回”当前栈帧(或直接在帧内访问,取决于优化) - 通用寄存器(
rax,rbx等)不保存也不恢复:它们属于调用者上下文,协程恢复后继续使用当前线程的寄存器状态 - 唯一需要人工干预的是 FPU/SIMD 寄存器:如果协程挂起前用了
__m256计算且未对齐保存,恢复后可能出错——但这种情况极少见,现代编译器通常自动处理
真正容易被忽略的是:协程帧本身分配在堆上(默认),而帧里的对象构造/析构顺序依赖于挂起/恢复路径——一旦在 await_suspend 里抛异常,帧可能部分构造,析构逻辑就变得非常脆弱。


















