协程不是线程,co_await仅挂起/恢复控制流而不并发;实现异步需手动集成OS或库(如asio),并正确实现awaiter三函数;std::generator更安全但只支持co_yield;调试须严防非法resume和悬垂引用。

协程不是线程,co_await 不会自动并发
很多人一看到 co_await 就以为“这下能并行了”,结果发现 IO 还是串着跑、CPU 密集任务卡死主线程。C++20 协程本身不带调度器,co_await 只是挂起/恢复控制流,不启动新线程,也不切换栈——它只是让函数能中途暂停、稍后继续,类似生成器。
真正异步 IO(比如读文件、发 HTTP)需要你手动把操作交给 OS 或第三方库(如 libuv、asio),然后让 awaiter 在操作完成时通知协程恢复。否则写个 co_await some_io_op(),而 some_io_op() 内部还是阻塞调用,那就白搭。
- 别指望
std::this_thread::sleep_for配co_await实现“非阻塞延时”——它照样阻塞线程;要用asio::steady_timer一类可 await 的定时器 - 如果你用的是
std::experimental::coroutine_handle手写 promise,必须自己保证 resume 被调用在线程安全的上下文中,否则容易崩溃 - MSVC 和 GCC 对协程的支持细节不同:GCC 12+ 默认需
-fcoroutines,MSVC 19.30+ 要/std:c++20 /await,漏掉编译开关会报co_await未声明
怎么写一个可 co_await 的类型?关键在 await_ready、await_suspend、await_resume
不是所有对象都能直接 co_await,必须满足“awaiter 概念”:提供这三个成员函数。它们决定协程何时挂起、挂到哪、恢复后返回什么。
常见错误是把 await_suspend 返回 true 却没手动 resume,导致协程永远挂起;或者返回 void(即同步完成),却在 await_resume 里抛异常,结果异常被吞掉。
立即学习“C++免费学习笔记(深入)”;
-
await_ready()返回true→ 协程不挂起,直接执行后续代码;返回false→ 进入挂起流程 -
await_suspend(handle)若返回std::coroutine_handle{},表示由你接管 resume 时机;若返回void,系统会在当前线程立刻 resume(等价于同步) -
await_resume()的返回值就是co_await expr的结果,类型必须和它声明的一致,否则编译失败
示例(简化版延迟 awaiter):
struct delay_awaiter {
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {
// 把 h 交给 timer 回调,比如 asio::steady_timer::async_wait
}
int await_resume() const noexcept { return 42; }
};
用 std::generator 做数据流比手写协程更安全
如果你只是想按需生成一系列值(比如遍历树、解析大文件行、分页查数据库),std::generator<T> 是 C++23 标准化前最稳的选择——它基于协程,但屏蔽了 promise、awaiter 等底层细节,且 MSVC/GCC/Clang 都已支持(需开启 C++20)。
相比自己实现 promise_type,std::generator 自动管理内存和生命周期,不会因忘记 final_suspend 返回 std::suspend_always 而导致析构时 crash。
- 不能在
std::generator函数里捕获局部变量引用并 yield 出去,因为协程帧可能在 yield 后销毁,引用变悬垂 - 每个
std::generator对象只能遍历一次,重复 begin() 不会重放;需要多次遍历得重新构造 - 它不支持
co_await,只支持co_yield;想混用 await 和 yield?得自己写 promise_type
调试协程崩溃时,优先检查 coroutine_handle::done() 和 resume() 调用时机
协程崩溃最常见的原因是 resume 已结束的协程,或对空 handle 调用 resume()。而 coroutine_handle::done() 并非线程安全——多线程环境下它可能刚返回 false,协程就在另一线程里结束了。
不要靠 if (!h.done()) h.resume(); 来防护,这有竞态。正确做法是:resume 前确保 handle 有效,且仅由约定好的一方负责 resume(比如 IO 完成回调);如果需要多处可能 resume,用原子状态标记 + CAS 控制。
- GDB/LLDB 对协程帧支持有限:
bt可能看到operator co_await但看不到原始调用点,建议在关键 awaiter 里加日志或断点 - ASan 能捕获协程帧内的 use-after-free,但无法检测跨协程的逻辑错误(比如忘了 resume)
- Release 模式下,
coroutine_handle::from_address(nullptr)不会报错,但后续 resume 必崩——务必初始化 handle 为nullptr并判空
协程的复杂性不在语法,而在控制流分散后责任边界的模糊。谁创建、谁挂起、谁 resume、谁销毁,这几个问题没理清,代码越写越像定时炸弹。











