C++awaitable类型自定义等待

2026-04-11 20:35:31 1352阅读 0评论

自定义 awaitable:C++20 协程里,让 co_await 听你的话

刚写完一个 co_await http_client.get("/api"),结果编译报错:“no matching operator co_await”。你挠头翻文档,发现 http_client::get() 返回的既不是 std::future,也不是 std::experimental::coroutine_handle,而是一个叫 http_response_awaitable 的自定义类型——它没实现等待协议,co_await 就不认识它。

这不是 bug,是接口设计的“留白”。C++20 协程不强制你用标准库提供的 awaitable(比如 std::suspend_always),只要你满足三个约定,co_await 就会乖乖调你的代码。这恰恰是 C++ 协程最实在的自由:不绑架你用什么调度器、什么 I/O 框架,只管你是否诚实履约。

那这三个约定是什么?不是抽象概念,是三颗能敲进 .h 文件里的具体函数。

第一颗钉子:await_ready()
它得是个 const 成员函数,返回 bool。作用很直白:“我现在就能给出结果,还用等吗?”
比如你封装了一个本地缓存查询,命中了就直接返回 trueco_await 会跳过挂起,继续往下走;没命中才走后续流程。别小看这个判断——它省掉一次协程挂起/恢复的开销,对高频小请求影响明显。

第二颗钉子:await_suspend(std::coroutine_handle<P>)
这是真正的“交班时刻”。参数是当前协程的句柄,你拿到它,就可以做三件事:

  • 把它存起来,等 I/O 完成后手动 resume()
  • 交给线程池或事件循环排队;
  • 甚至直接 resume() 它(相当于同步执行,但语义仍是协程);
  • 或者返回 false,告诉编译器:“别挂起了,我已处理完毕”。

关键点在于:返回值决定控制流走向。返回 voidbooltrue 表示真挂起,false 表示不挂起),返回 std::coroutine_handle<> 则表示把控制权转交给另一个协程——这是实现 await_transform 或链式 await 的伏笔,但日常自定义中,bool 最常用也最可控。

第三颗钉子:await_resume()
它在协程恢复后被调用,负责“交货”:把最终结果(比如 intstd::string、甚至 std::expected<T, E>)原样吐给 co_await 左边的变量。注意:它不能抛异常(除非你明确想让协程异常退出),更不能访问已销毁的栈变量——因为挂起期间,原始栈帧可能早已 unwind。

举个真实场景:你正在写一个轻量数据库客户端,想让 co_await db.execute("SELECT ...") 返回 std::vector<row>。你可以这样组织:

struct query_awaitable {
    std::string sql;
    database& db;

    bool await_ready() const noexcept {
        // 简单策略:本地语法检查通过就算 ready(避免网络往返)
        return is_select_statement(sql) && !sql.empty();
    }

    template<typename Promise>
    std::coroutine_handle<> await_suspend(std::coroutine_handle<Promise> h) {
        // 异步提交 SQL,完成时调用 h.resume()
        db.async_execute(sql, [h](auto rows) mutable {
            // 把结果暂存到 promise 中(需 Promise 类型提供 set_result)
            h.promise().set_result(std::move(rows));
            h.resume();
        });
        return std::noop_coroutine(); // 挂起,由回调唤醒
    }

    std::vector<row> await_resume() {
        // resume 后,从 promise 中取出结果
        return std::move(co_await_promise_result); // 实际需通过 promise 成员访问
    }
};

这里有个容易踩的坑:await_resume() 里不能直接访问 sqldb 的非常量引用——如果 query_awaitable 是临时对象(绝大多数情况),它在 await_suspend 返回后就被析构了。所有需要跨挂起保存的数据,必须挪到 promise 对象里,或用 std::shared_ptr 包裹

再补一个实用技巧:如果你的类型想支持 co_await xco_await x | then(...) 这种管道式写法,只需额外提供一个 operator| 重载,把右操作数包装进新的 awaitable——不需要改核心三函数,扩展性就出来了。

有人问:“非得写这三个函数?不能偷懒用 std::future 吗?”
可以,但代价是:std::future 要求 get() 阻塞等待,而协程挂起是无栈切换,零系统调用;std::futurethen() 是延迟绑定,而自定义 awaitable 可以在 await_suspend 里直接注册回调,减少一层间接跳转。性能差异在毫秒级服务里就是吞吐量的分水岭

最后说一句实在话:写自定义 awaitable 不是为了炫技,而是当你发现标准库的 std::generator 太重、std::stop_token 解耦不够、或者手头的异步 SDK 返回的是裸回调时——你手里那把 co_await 的钥匙,终于能插进自己造的锁孔里了。

协程的价值,不在语法多酷,而在你能否用它把“等一件事做完”这件事,说得比 std::thread 更准、比 std::future 更轻、比回调地狱更直。
而自定义 awaitable,就是那个让你把话说准、说轻、说直的动词。

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

发表评论

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

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

目录[+]