C++co_await协程等待表达式

2026-04-11 20:55:29 812阅读 0评论

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() 返回 falseawait_resume() 就一定会被调用。

很多人踩坑,是因为把 await_suspend() 当成“必须挂起”的铁律。错。它的签名是 boolvoid,若返回 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,到底想怎么交接控制权?

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
验证码
评论列表 (暂无评论,812人围观)

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

目录[+]