C++20协程是编译器驱动的状态机机制,协程帧存局部变量、状态枚举、promise指针及异常指针,默认堆分配且生命周期独立于调用栈;promise_type控制启停行为;co_await触发三步awaiter协议;无内置调度器,需手动管理coroutine_handle生命周期与唤醒时机。

协程在 C++20 里不是运行时库,也不是语法糖——它是一套编译器驱动的状态机生成机制,所有挂起、恢复、返回、异常处理,都靠编译器把函数重写成带状态跳转的类成员函数。你写的 co_await 看似简单,背后是 promise 对象、协程帧、coroutine_handle 三者协作的结果,没有调度器,协程根本不会动。
协程帧(coroutine frame)到底存了什么
协程帧是编译器为每个协程调用分配的一块内存,它不是栈,也不是堆上的任意对象,而是由编译器按需布局的结构体。里面至少包含:
- 所有局部变量和形参(哪怕只是
int x = 42;,也会被挪进帧里) - 当前执行状态(即状态机的枚举值,比如
state::start、state::await_suspended) - 指向
promise_type实例的指针(通常内嵌在帧头部) - 可能还有
std::exception_ptr存储未处理异常
关键点:帧默认在堆上分配(通过 operator new),除非你显式提供自定义分配器;它生命周期独立于调用栈——函数返回后帧还活着,直到协程最终被销毁。这也是为什么 co_await 后还能访问局部变量,但若忘了 co_return 或没正确处理 final_suspend,就容易悬垂。
promise_type 是协程行为的开关面板
每个协程类型(比如 task)必须关联一个 promise_type,它不参与业务逻辑,只决定协程怎么启、怎么停、怎么交结果。常见成员函数的实际影响:
-
initial_suspend():返回std::suspend_always表示协程创建后立刻挂起,适合 lazy 启动;返回std::suspend_never则立即执行首段代码——但若首段就co_await,仍会挂起,只是时机晚一步 -
final_suspend():多数人设为std::suspend_always,否则协程结束时帧可能被立即释放,导致coroutine_handle指向野内存 -
return_value(T&&)和unhandled_exception():这两个函数不抛异常,但若实现里写了throw,就会触发未定义行为——因为协程销毁路径上不能再抛异常
漏掉任何一个,编译器都会报错;写错语义(比如 final_suspend 返回 std::suspend_never 又不手动 destroy()),运行时大概率 crash。
co_await 不是等待,是状态机分支跳转
co_await expr 展开后,实际执行三步:expr.await_ready() → 若为 false,则调 expr.await_suspend(handle) → 最后调 expr.await_resume()。整个过程不阻塞线程,也不依赖系统时钟或事件循环——它只是让出控制权,把 handle 交给别人保管。
- 如果
await_suspend返回void或false,协程立即挂起,控制权回到调用方 - 如果返回
true,协程挂起,但调用方不能直接返回,必须等别人调handle.resume() - 如果返回另一个
coroutine_handle,就触发“对称转移”(symmetric transfer),控制流直接跳过去,原协程帧不再恢复——这是零开销链式调度的关键,但也意味着你得确保那个 handle 有效且未被销毁
很多人卡在 await_suspend 返回 true 后忘记 resume,结果协程永远沉睡;更隐蔽的是,在 lambda 里捕获了局部变量又传给 await_suspend,等 resume 时变量早已析构。
为什么 C++20 协程必须自己写调度器
标准只规定了“如何挂起”,没规定“谁来唤醒”。co_await 后的 awaitable 对象可以是 std::future、自定义 socket wrapper、甚至空操作,但唤醒逻辑全得你填。典型错误包括:
- 用
std::thread+notify_one()唤醒,却没配std::condition_variable的锁保护,导致唤醒丢失 - 在 IO 完成回调里直接
handle.resume(),但该 handle 所属协程帧正在被另一个线程销毁,引发 double-free - 把
coroutine_handle存在栈上(比如函数参数),resume 前函数已返回,handle 成悬垂指针
真正落地时,90% 的问题不出在 co_await 写法,而出在 handle 生命周期管理与调度时机的错配——它不像 Go 那样自动绑定 goroutine 到 M/P,C++ 的每个 handle 都是你亲手托付出去的借条,收不回来就是资源泄漏或崩溃。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











