C++awaitable类型自定义等待
自定义 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。作用很直白:“我现在就能给出结果,还用等吗?”
比如你封装了一个本地缓存查询,命中了就直接返回 true,co_await 会跳过挂起,继续往下走;没命中才走后续流程。别小看这个判断——它省掉一次协程挂起/恢复的开销,对高频小请求影响明显。
第二颗钉子:await_suspend(std::coroutine_handle<P>)
这是真正的“交班时刻”。参数是当前协程的句柄,你拿到它,就可以做三件事:
- 把它存起来,等 I/O 完成后手动
resume(); - 交给线程池或事件循环排队;
- 甚至直接
resume()它(相当于同步执行,但语义仍是协程); - 或者返回
false,告诉编译器:“别挂起了,我已处理完毕”。
关键点在于:返回值决定控制流走向。返回 void 或 bool(true 表示真挂起,false 表示不挂起),返回 std::coroutine_handle<> 则表示把控制权转交给另一个协程——这是实现 await_transform 或链式 await 的伏笔,但日常自定义中,bool 最常用也最可控。
第三颗钉子:await_resume()
它在协程恢复后被调用,负责“交货”:把最终结果(比如 int、std::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() 里不能直接访问 sql 或 db 的非常量引用——如果 query_awaitable 是临时对象(绝大多数情况),它在 await_suspend 返回后就被析构了。所有需要跨挂起保存的数据,必须挪到 promise 对象里,或用 std::shared_ptr 包裹。
再补一个实用技巧:如果你的类型想支持 co_await x 和 co_await x | then(...) 这种管道式写法,只需额外提供一个 operator| 重载,把右操作数包装进新的 awaitable——不需要改核心三函数,扩展性就出来了。
有人问:“非得写这三个函数?不能偷懒用 std::future 吗?”
可以,但代价是:std::future 要求 get() 阻塞等待,而协程挂起是无栈切换,零系统调用;std::future 的 then() 是延迟绑定,而自定义 awaitable 可以在 await_suspend 里直接注册回调,减少一层间接跳转。性能差异在毫秒级服务里就是吞吐量的分水岭。
最后说一句实在话:写自定义 awaitable 不是为了炫技,而是当你发现标准库的 std::generator 太重、std::stop_token 解耦不够、或者手头的异步 SDK 返回的是裸回调时——你手里那把 co_await 的钥匙,终于能插进自己造的锁孔里了。
协程的价值,不在语法多酷,而在你能否用它把“等一件事做完”这件事,说得比 std::thread 更准、比 std::future 更轻、比回调地狱更直。
而自定义 awaitable,就是那个让你把话说准、说轻、说直的动词。


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