用 await_transform 定制协程的挂起行为
协程是 C++20 引入的零开销抽象,让你能用同步风格写异步代码。co_await 是挂起和恢复的关键,而 await_transform 是 promise_type 中的一个特殊方法,允许你拦截 co_await 的表达式,重新定义挂起逻辑。本文直接聚焦 await_transform 的实战用法。
1. 理解 co_await 的基础挂起流程
co_await expr 要求 expr 必须是一个 Awaitable 对象(实现了 await_ready、await_suspend、await_resume)。标准写法是通过 promise_type 的 await_transform 来改变这个流程。
默认情况下,编译器会尝试在 promise_type 中寻找 await_transform 的重载。如果找到,就会用 await_transform(expr) 的返回值作为 Awaitable 对象。否则直接使用 expr 本身。这给了你完全控制 co_await 行为的能力。
2. 定义 await_transform 钩子
步骤1: 在 promise_type 中声明一个或多个 await_transform 重载。参数类型决定哪些表达式会被拦截。
struct MyPromise {
// 拦截 int 类型的表达式
auto await_transform(int value) {
struct Awaiter {
int v;
bool await_ready() const noexcept { return false; } // 总是挂起
void await_suspend(std::coroutine_handle<>) {
// 挂起逻辑:保存 value,例如投递给 io_context
std::cout << "挂起,int = " << v << std::endl;
}
int await_resume() const noexcept { return v * 2; } // 恢复时返回值
};
return Awaiter{value};
}
// 拦截 std::chrono::duration,实现延时挂起
template<typename Rep, typename Period>
auto await_transform(std::chrono::duration<Rep, Period> dur) {
struct Awaiter {
std::chrono::duration<Rep, Period> d;
bool await_ready() const noexcept { return d.count() <= 0; }
void await_suspend(std::coroutine_handle<> handle) {
// 启动定时器,到时后调用 handle.resume()
auto timer = std::make_shared<std::thread>([handle, this] {
std::this_thread::sleep_for(d);
handle.resume();
});
timer->detach();
}
void await_resume() const noexcept {}
};
return Awaiter{dur};
}
// 拦截自定义类型
auto await_transform(MyEvent& event) {
// ...
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
步骤2: 在协程函数中使用 co_await,编译器自动匹配 await_transform 重载。
MyTask example() {
int result = co_await 42; // 调用 int 版本,挂起后 result = 84
co_await std::chrono::seconds(2); // 延时 2 秒
std::cout << "resumed" << std::endl;
}
3. 自定义挂起逻辑的三种典型场景
3.1 转换返回值类型或附加行为
通过 await_transform 你可以在挂起/恢复时注入额外操作。例如从 int 扩展为 int*2,或者记录日志。
3.2 将非 Awaitable 对象转换为 Awaitable
标准库的 std::future 不是原生 Awaitable,但可以通过 await_transform 包装使其可 co_await。类似地,回调、信号量、网络套接字等都能变成协程友好的对象。
// 将回调转换为可 co_await
auto await_transform(std::function<void(std::function<void()>)> callback) {
struct Awaiter {
std::function<void(std::function<void()>)> cb;
std::coroutine_handle<> handle;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
handle = h;
cb([this] { handle.resume(); });
}
void await_resume() {}
};
return Awaiter{callback};
}
使用:
co_await [](auto resume) {
std::thread([resume] {
std::this_thread::sleep_for(1s);
resume();
}).detach();
};
// 一秒钟后继续
3.3 条件挂起或动态决定是否挂起
在 await_ready 中可以根据运行时状态决定是否立即恢复。结合 await_transform 的参数,可以实现灵活控制。
auto await_transform(Request& req) {
return Awaiter{req};
}
struct Awaiter {
Request& req;
bool await_ready() {
return req.cached(); // 有缓存则不挂起
}
void await_suspend(std::coroutine_handle<> h) {
req.start_async(h); // 异步启动,完成后恢复
}
Result await_resume() {
return req.result();
}
};
4. 多级 await_transform 与优先级
如果 await_transform 有多个重载,编译器会按常规重载决议选择最匹配的版本。你也可以利用 SFINAE 或概念来限制模板重载。
template<typename T>
requires std::is_integral_v<T>
auto await_transform(T val) { ... } // 所有整数类型
如果不想拦截某些类型,就不提供对应重载,编译器会直接用表达式本身作为 Awaitable。例如保留 std::suspend_always 的原始行为。
5. 常见陷阱与规避
- 忘记处理异常:
await_suspend中如果抛出异常会直接终止程序。确保异常安全,例如在定时器线程中捕获异常并通知协程。 - 生命周期:
await_transform返回的 Awaiter 对象生命周期必须跨越挂起期间。通常将其按值返回,拷贝或移动即可,但注意内部引用不能悬挂。 - 多线程同步:
await_suspend可能在其他线程执行,访问共享数据需要加锁或用原子操作。 - 不要与
operator co_await混用:如果表达式自身已有operator co_await,编译器会优先使用await_transform还是成员operator co_await?标准规定:先找await_transform,如果不存在再找表达式上的operator co_await。因此await_transform优先级更高。
6. 完整示例:简单的调度器
#include <coroutine>
#include <iostream>
#include <thread>
#include <chrono>
#include <vector>
#include <functional>
struct Task {
struct promise_type {
auto await_transform(int val) {
struct Awaiter {
int v;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<>) {
std::cout << "int " << v << " handled\n";
}
int await_resume() { return v * 10; }
};
return Awaiter{val};
}
auto await_transform(auto&&) = delete; // 拒绝其他类型
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
Task get_return_object() { return Task{std::coroutine_handle<promise_type>::from_promise(*this)}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle;
explicit Task(std::coroutine_handle<promise_type> h) : handle(h) {}
~Task() { if (handle) handle.destroy(); }
};
Task demo() {
int x = co_await 5; // x = 50
std::cout << "x = " << x << std::endl;
// co_await 3.14; // 编译错误,被 =delete 拦截
}
int main() {
demo();
return 0;
}
7. 性能考量
await_transform 本身没有额外开销(编译器在编译期完成类型转换)。Awaiter 的每个成员函数都是内联候选。唯一可能的性能损耗来自 await_suspend 中的动态分配(如 new 定时器)。你可以用 noexcept 声明 await_suspend 来让编译器产生更高效的代码。
记住:await_transform 是协程框架的核心扩展点。它让你在不修改已有类型的情况下,无缝地将 co_await 移植到你的异步模型中。花几分钟设计好 await_transform 的重载集合,就能省下大量重复的 Awaiter 定义代码。

暂无评论,快来抢沙发吧!