C++20 中的协程:如何使用和常见陷阱

在 C++20 标准中,协程(coroutine)被正式纳入语言规范,提供了一种优雅的方式来实现异步编程、生成器和并发控制。本文将从协程的基本概念、关键字与类型、使用步骤以及常见陷阱四个方面进行阐述,帮助读者快速上手并避免常见错误。


1. 协程的基本概念

协程是一种能够暂停和恢复执行的函数。不同于传统的线程,协程在同一线程中协作完成任务,切换成本极低。协程通过 挂起(suspend)恢复(resume) 的机制,隐藏了底层的状态机实现。

C++20 将协程拆分为三部分:

  1. 协程函数(coroutine function),使用 co_await, co_yield, co_return 关键字。
  2. 协程生成器(promise type),定义协程的生命周期与返回值。
  3. 协程句柄(coroutine handle),用于手动恢复、检查状态或销毁协程。

2. 关键字与相关类型

关键字 用途 备注
co_await 暂停协程,等待一个 awaitable 对象完成 只能在协程内部使用
co_yield 暂停协程并返回一个值给调用者 生成器函数常用
co_return 结束协程并返回最终结果 只能在协程内部使用
co_await 的可等待对象(awaitable) 必须满足 await_ready, await_suspend, await_resume 三个成员函数 也可以是 std::futurestd::experimental::future

核心类型:

  • `std::coroutine_handle `:协程句柄,`PromiseT` 是对应的 promise 类型。
  • std::suspend_always / std::suspend_never:内置的 awaitable,用于控制协程的挂起与恢复。

3. 一个完整的协程示例

下面给出一个简单的整数序列生成器(类似 iota),使用 co_yield 输出每个值。

#include <coroutine>
#include <iostream>
#include <optional>

struct Iota {
    struct promise_type {
        std::optional <int> value;           // 当前产出值
        Iota get_return_object() {          // 返回协程句柄
            return Iota{std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; } // 立即挂起,等待 resume
        std::suspend_always final_suspend() noexcept { return {}; } // 结束后挂起
        void unhandled_exception() { std::terminate(); }
        void return_void() {}
    };

    std::coroutine_handle <promise_type> handle; // 内部句柄

    explicit Iota(std::coroutine_handle <promise_type> h) : handle(h) {}
    ~Iota() { if (handle) handle.destroy(); }

    struct iterator {
        std::coroutine_handle <promise_type> h;
        bool first = true;

        iterator(std::coroutine_handle <promise_type> h) : h(h) {}
        iterator& operator++() { // 触发协程恢复
            h.resume();
            return *this;
        }
        std::optional <int> operator*() const { return h.promise().value; }
        bool operator!=(const iterator& rhs) const { return h != rhs.h; }
    };

    iterator begin() {
        handle.resume();                 // 第一次恢复,执行到第一个 co_yield
        return iterator{handle};
    }
    iterator end() { return iterator{handle}; }
};

// 协程函数
Iota make_iota(int start, int count) {
    for (int i = 0; i < count; ++i) {
        co_yield start + i;          // 输出当前值
    }
}

int main() {
    for (int v : make_iota(5, 10)) { // 输出 5..14
        std::cout << v << ' ';
    }
    return 0;
}

关键点

  • promise_type::get_return_object() 返回一个包装了协程句柄的对象,供外部使用。
  • initial_suspend()final_suspend() 控制协程何时挂起与结束。
  • co_yield 将值放入 promise_type 的成员 value,随后挂起。

4. 常见陷阱与解决办法

陷阱 说明 解决办法
协程句柄未销毁 协程结束后句柄未显式销毁,导致资源泄漏 promise_type::final_suspend() 或者外部对象析构时调用 handle.destroy()
无限挂起 co_await 的 awaitable 没有实现 await_ready()await_resume(),导致永远挂起 确保 awaitable 对象实现所有三方法,且 await_ready() 能够返回 true 或者 false 以触发挂起
错误的协程返回类型 直接返回 intvoid,导致编译错误 协程函数返回 `std::coroutine_handle
` 或自定义包装类
多次 resume 后异常 对已结束的协程再次 resume,导致 std::bad_function_call 在每次 resume 前检查 handle.done()
协程不支持递归 递归调用同一协程会导致栈空间耗尽 通过循环或异步调度实现递归逻辑,避免深层嵌套

5. 结合 async/await 与 std::future

C++20 的 co_awaitstd::future 并不直接兼容。若想在协程中等待 std::future,需要自定义一个 awaitable 包装器:

template<typename T>
struct FutureAwaiter {
    std::future <T> fut;
    bool await_ready() const noexcept { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }
    void await_suspend(std::coroutine_handle<> h) {
        std::thread([h, f=std::move(fut)]() mutable {
            f.wait();
            h.resume();
        }).detach();
    }
    T await_resume() { return fut.get(); }
};

template<typename T>
FutureAwaiter <T> make_awaiter(std::future<T> f) { return FutureAwaiter<T>{std::move(f)}; }

使用时:

int result = co_await make_awaiter(std::async(std::launch::async, []{ return 42; }));

6. 小结

  • C++20 协程通过 co_await, co_yield, co_return 关键字以及 Promise/Handle 机制实现。
  • 关键点在于 promise_type 的实现和协程句柄的管理。
  • 典型的协程使用场景包括生成器、异步 I/O、任务调度等。
  • 关注资源释放、挂起条件和协程生命周期是避免常见错误的关键。

通过上述示例与技巧,读者应能快速构建自己的协程,并在项目中发挥其优势。祝编码愉快!

发表评论