C++co_await协程等待表达式
co_await 不是“挂起开关”,而是协程的「交接契约」
你写完一个 co_await 表达式,编译器没报错,程序跑起来却卡在那儿不动了——不是死锁,也不是阻塞,就是“悬”在那儿,像咖啡凉到一半、话说到半句。这种似懂非懂的“悬停感”,恰恰暴露了我们对 co_await 最常见的误解:把它当成一个魔法开关,一按就“暂停”,再按就“继续”。
其实,co_await 从不主动挂起。它只做一件事:询问 awaitable 对象——“我接下来该怎么做?”
这个“问”,才是整个协程等待机制的起点。
C++20 的协程不是运行时调度器,也没有内置线程池。它是一套可定制的控制流重写机制。当你写下 co_await expr,编译器会检查 expr 是否满足 awaitable 要求:即是否提供 await_ready()、await_suspend() 和 await_resume() 这三个成员函数(或 ADL 查找到的对应自由函数)。
这三者构成一份轻量但严谨的「交接契约」:
await_ready()是个快速探针:它返回true,协程立刻继续执行,根本不会挂起。比如std::future<T>::then风格的立即就绪 future,或者自定义的immediate_awaitable,就靠它绕过调度开销;await_suspend()是真正的“交接时刻”:它接收一个coroutine_handle参数,代表当前协程的控制权。你可以把它交给线程池、IO 多路复用器、甚至另一个协程;也可以直接resume()它——那就不挂起了,变成同步调用;await_resume()是“回来后拿什么”:它定义co_await expr整体表达式的返回值。注意:它不是恢复点,而是结果出口。哪怕await_suspend()里啥都没干,只要await_ready()返回false,await_resume()就一定会被调用。
很多人踩坑,是因为把 await_suspend() 当成“必须挂起”的铁律。错。它的签名是 bool 或 void,若返回 true,协程挂起;返回 false,编译器立刻调用 await_resume() 并继续执行——中间不切换栈、不保存上下文。这个 bool 返回值,就是你掌控“是否真挂起”的第一道阀门。
举个接地气的例子:写一个带超时的 co_await socket.read()。
你不想让协程永远等下去,但也不想每毫秒轮询一次。怎么办?
可以设计一个 awaitable_with_timeout 包装器,在 await_suspend() 中启动一个定时器,并把当前协程 handle 存进去;同时注册 socket 可读事件回调。任一事件触发,都调用 handle.resume()。
关键来了:如果 socket 此刻已就绪(比如内核缓冲区有数据),await_ready() 就该返回 true,跳过定时器和回调注册——零开销完成等待。
这才是 await_ready() 的真实价值:不是“优化可选项”,而是“避免无谓调度”的必要判断。
再看常见误区:有人把 std::this_thread::sleep_for 直接 co_await,结果编译失败。因为 std::chrono::duration 不是 awaitable。这不是语法限制,而是语义拒绝——休眠不该由协程框架接管,而应由调度器统一管理。正确做法是封装一个 sleep_for_awaitable(ms),在 await_suspend() 中交由 io_uring 或 epoll 定时器处理。
还有一点常被忽略:co_await 表达式本身有求值顺序。它等价于:
auto&& __awaitable = (expr);
if (!__awaitable.await_ready()) {
if (__awaitable.await_suspend(handle)) {
// 挂起,控制权移交
return; // 协程退出当前帧
}
}
// 未挂起,或 await_suspend 返回 false
return __awaitable.await_resume();
注意:await_resume() 总在 await_suspend() 之后调用(哪怕没挂起),且它不能抛异常(除非你显式声明 noexcept(false) 并处理)。一旦 await_resume() 抛异常,它会直接沿协程调用栈向上冒泡——和普通函数调用一样自然,无需特殊语法。
最后说个实用技巧:调试协程挂起点。别只盯着 co_await 行号。真正决定是否挂起的,是 await_ready() 的返回值。建议在自定义 awaitable 中加日志:
bool await_ready() const noexcept {
auto r = /* 实际判断 */;
std::cout << "[debug] await_ready -> " << (r ? "true" : "false") << "\n";
return r;
}
你会发现,很多“卡住”的问题,根源是 await_ready() 总返回 false,而 await_suspend() 又忘了 resume——协程就这么静静躺在内存里,成了幽灵任务。
co_await 不是语法糖,它是你和协程运行时之间的一纸契约。签得清楚,它就听话;签得含糊,它就沉默。
写协程代码,与其反复试错,不如先问自己一句:这个 awaitable,到底想怎么交接控制权?


还没有评论,来说两句吧...