**题目:C++20 协程的自定义 awaiter 如何实现**

在 C++20 中,协程已成为语言的一部分,但默认的 co_await 只支持 std::suspend_alwaysstd::suspend_never、以及符合 Awaitable 协议的对象。若想在协程中使用自定义的等待逻辑(比如基于事件驱动的任务调度器、异步 IO 等),就需要实现自己的 awaiter。本文将详细讲解如何实现一个自定义 awaiter,包含基本接口、状态管理以及与协程句柄的交互。


1. Awaitable 的基本要求

要让一个对象可 co_await,它必须满足以下成员:

成员 说明 返回类型
bool await_ready() 判断是否需要挂起 bool
void await_suspend(std::coroutine_handle<>) 挂起时的操作 voidbool(若返回 true 则挂起,false 直接继续)
decltype(auto) await_resume() 协程恢复后返回值 任意

此外,协程句柄类型取决于协程的返回类型。若返回 void,则使用 std::coroutine_handle<>;若返回 T,则使用 std::coroutine_handle<promise_type>


2. 设计自定义 Awaiter

假设我们要实现一个基于事件循环的 TimerAwaiter,在指定时间后恢复协程。下面是基本结构:

#include <chrono>
#include <coroutine>
#include <functional>
#include <unordered_set>

template <typename Clock = std::chrono::steady_clock>
class TimerAwaiter {
public:
    using duration = typename Clock::duration;
    explicit TimerAwaiter(duration d) : delay_(d) {}

    bool await_ready() const noexcept { return delay_ == duration::zero(); }

    void await_suspend(std::coroutine_handle<> h) {
        // 将协程句柄与超时时间一起加入全局事件循环
        get_event_loop().schedule(h, Clock::now() + delay_);
    }

    void await_resume() const noexcept { /* 无返回值 */ }

private:
    duration delay_;

    // 简单事件循环(单线程)
    static struct EventLoop {
        // map 事件时间 -> 句柄列表
        std::multimap<Clock::time_point, std::coroutine_handle<>> timers_;
        std::unordered_set<std::coroutine_handle<>> active_;

        void schedule(std::coroutine_handle<> h, Clock::time_point when) {
            timers_.emplace(when, h);
            active_.insert(h);
        }

        // 每帧或在单独线程中调用
        void tick() {
            auto now = Clock::now();
            auto it = timers_.begin();
            while (it != timers_.end() && it->first <= now) {
                it->second.resume();          // 恢复协程
                active_.erase(it->second);    // 移除
                it = timers_.erase(it);       // 删除计时器
            }
        }
    } & get_event_loop() {
        static EventLoop loop;
        return loop;
    }
};

说明:

  • await_ready() 在延迟为 0 时直接完成,避免无用挂起。
  • await_suspend() 将协程句柄 h 与未来的触发时间一起注册到事件循环。
  • 事件循环的 tick() 在主线程或单独线程中周期性调用,用来检查已到时间的协程并恢复。

3. 在协程中使用

#include <iostream>
#include <chrono>

struct Sleep {
    std::chrono::milliseconds ms;
};

Sleep operator"" _ms(unsigned long long v) {
    return Sleep{std::chrono::milliseconds(static_cast<std::chrono::milliseconds::rep>(v))};
}

auto worker() -> std::generator <void> { // C++20 std::generator (C++23 以后)
    std::cout << "Start\n";
    co_await TimerAwaiter<std::chrono::steady_clock>(1000_ms);
    std::cout << "After 1s\n";
    co_await TimerAwaiter<std::chrono::steady_clock>(500_ms);
    std::cout << "After 0.5s\n";
}

int main() {
    auto gen = worker();
    while (gen) {
        gen.resume();   // 手动驱动
        TimerAwaiter<>:get_event_loop().tick(); // 触发事件
    }
}

此示例演示了如何在协程中使用自定义 awaiter。TimerAwaiter 可以替换为任何符合 Awaitable 接口的对象,例如基于 I/O 的 AsyncReadAwaiter、网络请求的 HttpAwaiter 等。


4. 与 Promise 结合

若协程返回值非 void,则需要实现 promise_type。下面给出一个 `Task

` 的简易实现示例: “`cpp template class Task { public: struct promise_type { T value_; std::exception_ptr exc_; auto get_return_object() { return Task{std::coroutine_handle ::from_promise(*this)}; } std::suspend_never initial_suspend() { return {}; } std::suspend_never final_suspend() noexcept { return {}; } void return_value(T v) { value_ = std::move(v); } void unhandled_exception() { exc_ = std::current_exception(); } }; explicit Task(std::coroutine_handle h) : handle_(h) {} ~Task() { if (handle_) handle_.destroy(); } T get() { if (handle_.promise().exc_) std::rethrow_exception(handle_.promise().exc_); return handle_.promise().value_; } private: std::coroutine_handle handle_; }; “` 与 `TimerAwaiter` 结合: “`cpp Task delayed_sum() { int a = 1; co_await TimerAwaiter(300_ms); int b = 2; co_await TimerAwaiter(200_ms); co_return a + b; // 5 } “` — ### 5. 常见陷阱与最佳实践 | 陷阱 | 解决方案 | |——|———-| | **协程句柄泄露** | 在 `promise_type::final_suspend` 中确保所有资源销毁,或使用 `std::unique_ptr` 包装句柄。 | | **多线程挂起/恢复冲突** | 在多线程环境下,`await_suspend` 与事件循环必须线程安全;使用 `std::mutex` 或无锁数据结构。 | | **长时间挂起导致堆栈消耗** | 在事件循环外部维护任务队列,避免协程自身递归。 | | **事件循环未被调用** | 确保 `tick()` 被周期性调用,或在主线程中加入 `while (event_loop.poll()) {}` 循环。 | — ### 6. 小结 自定义 awaiter 为 C++20 协程提供了极大的灵活性。通过实现 `await_ready`、`await_suspend`、`await_resume` 三个成员函数,并配合事件循环或线程池,你可以轻松地把异步 IO、计时器、网络请求等功能集成到协程中。掌握这些概念后,协程将不再是神秘的“黑盒”,而是可以被完全控制的异步工具。 希望本文能帮助你快速上手自定义 awaiter,并在自己的项目中实现更高效、更可读的异步代码。

发表评论