C++协程在编译期被转换为带switch(__state)的状态机,局部变量提升至堆上协程帧,__state为整型游标,由promise_type控制挂起时机,co_await触发await_ready/await_suspend/await_resume三阶段调用。

协程函数一编译就变成状态机,不是运行时动态构建
你写的 task 在编译阶段就被 clang/g++ 拆成一个带 switch(__state) 的类——它根本不是“启动后才生成状态机”,而是源码到汇编之间,编译器硬生生给你手写了一个状态跳转逻辑。
- 每个
co_await、co_yield、co_return都对应一个挂起点(suspend point),编译器据此划分状态编号:0 是入口,1 是第一个co_await后,2 是第二个…… - 局部变量(如
int x = 5;)不会存在栈上,而是被提升(promoted)进协程帧(coroutine frame)堆内存里——否则挂起后栈被回收,恢复时就访问野指针 -
__state字段不公开,但你能通过h.done()或调试器观察其值变化,它本质就是个整型状态游标
promise_type 决定状态流转时机,不是“配个壳就行”
协程是否一启动就挂起?结束前要不要再挂一次?这些全由你定义的 promise_type::initial_suspend() 和 final_suspend() 返回值控制。返回 std::suspend_always{} 就强制挂,返回 std::suspend_never{} 就直奔执行——但错配会直接导致未定义行为。
- 常见错误:
initial_suspend()返回std::suspend_never{},但 promise 中没实现get_return_object(),编译失败报错指向不明的模板实例化深度 - 真实场景:网络请求协程常设
initial_suspend() → suspend_always,让调度器先拿到 handle 再决定何时 resume;而生成器(generator)往往用suspend_never确保首次调用立即进入首段逻辑 -
final_suspend()若返回suspend_always,协程结束时不自动销毁帧,必须手动调用h.destroy(),漏掉就会内存泄漏
co_await 触发的三个钩子,才是状态切换的实际开关
co_await expr 不是语法糖,它是编译器插入的三段确定性调用:expr.await_ready() → expr.await_suspend(h) → expr.await_resume()。中间任意一环出错,状态机就卡死。
-
await_ready()返回true:不挂起,直接执行await_resume(),相当于“同步穿透” -
await_suspend(h)若返回void或bool:true 表示已接管调度,协程真挂起;false 表示拒绝挂起,继续执行 - 最易踩坑:
await_suspend()里误调h.resume()会导致栈溢出——因为此时协程还没真正挂起,resume 相当于递归重入 - 标准库提供的
std::suspend_always和std::suspend_never就是为快速验证这三步行为而生,别绕开它们自己造轮子
无栈 ≠ 无状态,所有“暂停”本质都是 goto + 堆保存
C++ 协程是无栈的,但它绝不是无状态的。所谓“无栈”,只是不分配独立栈空间;所谓“状态机”,就是把函数切片 + 把变量搬家 + 用整数记位置——全部靠编译器在编译期完成。
- 性能关键点:每次
co_await都要 new 一块协程帧(含 promise + 局部变量 + 编译器私有字段),高频短协程容易触发小内存频繁分配,建议用内存池(如std::pmr::polymorphic_allocator)接管 - 调试难点:GDB/LLDB 对协程支持仍弱,
__state值和当前实际执行行常不一致;更可靠的方式是给 promise 加日志,在await_suspend和final_suspend里打点 - 兼容性注意:MSVC 对协程 ABI 支持与 GCC/Clang 存在细微差异,跨编译器传递
std::coroutine_handle时,务必用void*转换并文档注明限制
协程状态机的“状态”不是抽象概念,它是编译器写死的整数、堆上固定的内存布局、以及 promise 中那几个必须重载的函数——少一个,整个机器就转不动。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











