文章目录

C++ 协程 co_await 关键字的 await_transform 钩子自定义挂起逻辑

发布于 2026-05-28 22:20:00 · 浏览 52 次 · 评论 0 条

await_transform 定制协程的挂起行为

协程是 C++20 引入的零开销抽象,让你能用同步风格写异步代码。co_await 是挂起和恢复的关键,而 await_transformpromise_type 中的一个特殊方法,允许你拦截 co_await 的表达式,重新定义挂起逻辑。本文直接聚焦 await_transform 的实战用法。


1. 理解 co_await 的基础挂起流程

co_await expr 要求 expr 必须是一个 Awaitable 对象(实现了 await_readyawait_suspendawait_resume)。标准写法是通过 promise_typeawait_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 定义代码。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文