c++20协程是编译器与运行时协作的状态机机制,非语法糖;co_await等关键字触发编译器生成挂起/恢复逻辑,但调度、内存管理、线程切换需手动实现。

协程不是语法糖,是编译器+运行时协作的机制
直接说结论:C++20 的 co_await、co_yield、co_return 本身不提供调度器或线程切换,它们只是让编译器生成状态机代码。你写一个 co_await 表达式,编译器会把它拆成「挂起前保存现场」「恢复时跳回正确位置」两部分,但谁来保存栈、谁来唤醒、在哪执行——全得你自己管。
常见错误现象:std::coroutine_handle 拿到后直接 .resume() 却崩溃;或者协程函数返回后立刻访问局部变量,结果读到垃圾值——因为栈帧早被回收了。
- 所有协程对象(比如
Task类型)必须自己管理内存生命周期,不能依赖栈分配 -
co_await右值必须实现await_ready/await_suspend/await_resume三个成员函数,缺一不可 - 如果你没写自定义
promise_type,编译器会用默认空实现,但那样co_return后无法取返回值,也捕获不到异常
怎么写一个最小可运行的 Task 协程类型
目标很实际:让函数能用 co_await 等待另一个协程,并拿到它的返回值。关键在 promise_type 的设计。
使用场景:写异步 I/O 封装、测试协程调度逻辑、理解挂起/恢复链路。
立即学习“C++免费学习笔记(深入)”;
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
void return_value(int v) { value = v; }
int value = 0;
};
};
注意点:
-
initial_suspend()返回std::suspend_never表示协程创建后立即执行,不是懒启动 -
final_suspend()必须返回std::suspend_always,否则协程结束时 handle 自动销毁,你再拿它就 UB - 返回值存在
promise_type成员里,不是靠栈传参;所以Task对象本身要持有std::coroutine_handle<promise_type></promise_type>才能读value
co_await 之后到底发生了什么?别猜,看 suspend 调用时机
很多人以为 co_await expr 是“等 expr 完成”,其实它只做三件事:调用 expr.await_ready() → 若返回 false,调用 expr.await_suspend(handle) → 挂起当前协程。
容易踩的坑:
-
await_suspend返回true:表示你已接管调度,编译器不会再自动 resume;返回false或 void:编译器会在当前线程立刻 resume - 如果
await_suspend把handle交给另一个线程去resume(),那它必须确保该 handle 在 resume 前不被销毁(比如用std::shared_ptr包一层) -
await_resume()的返回值就是co_await expr整个表达式的值,类型必须严格匹配,否则编译失败
为什么调试协程时断点经常跳错位置?
因为编译器把协程函数重写成了巨型 switch-case 状态机,所有 co_await 都变成 goto 跳转。调试器看到的“当前行”往往是状态机的 dispatcher,不是你写的源码行。
实操建议:
- 优先用
printf或日志打在await_suspend/await_resume里,比依赖 IDE 断点更可靠 - 不要对协程函数单步调试——切到汇编或反编译看状态编号更有效
- Clang 编译加
-g -O1比-O2更利于调试;GCC 下-Og是专为调试协程优化的选项
最麻烦的地方在于:协程的“堆栈”是分散在堆上的多个 promise 对象 + 手动维护的状态字段,没有传统调用栈可回溯。一旦 suspend/resume 链路断掉,就只能靠日志和 handle 生命周期检查来定位。










