C++20中的协程:从理论到实践

协程(Coroutines)是C++20中一个强大的特性,它让我们可以以一种自然、可读性极高的方式来处理异步编程、生成器以及更复杂的状态机。相比传统的回调和Future,协程的实现更为轻量,性能更佳。下面我们从概念入手,讲解协程的基本构造、关键字和一个小实例,帮助你快速上手。

1. 协程的基本概念

协程是一段可以在运行时暂停和恢复的函数。不同于线程,协程在单线程内完成切换,避免了上下文切换开销。C++协程的关键是 co_awaitco_yieldco_return,它们分别对应等待、产出和返回值。

  • co_await:等待一个可协程对象(awaiter),直到其完成后继续执行。
  • co_yield:从协程返回一个值,并暂停执行,随后可以再次 resume。
  • co_return:结束协程并返回最终值。

协程函数本身并不返回 void,而是返回一个特殊的 promise 对象,告诉编译器如何处理暂停、恢复、异常和返回值。

2. 协程的核心类型

C++20规定协程函数返回的类型必须满足 Awaitable 的要求。最常见的组合是:

std::future <T>   // 用于异步操作,返回 future<T>
std::generator <T> // 用于生成器,返回 generator<T>

在标准库中,std::generator 仍处于实验阶段,但大多数编译器都已支持。若要自己实现一个简易的 generator,可以参考下面的代码。

3. 一个完整的协程生成器示例

下面演示一个 int_generator,它一次产生从 1 开始的自然数序列,直到达到给定上限。

#include <iostream>
#include <coroutine>
#include <exception>

template<typename T>
struct generator {
    struct promise_type {
        T current_value;
        std::exception_ptr exc;

        auto get_return_object() {
            return generator{ std::coroutine_handle <promise_type>::from_promise(*this) };
        }
        std::suspend_always initial_suspend() { return {}; }   // 立即暂停
        std::suspend_always final_suspend() noexcept { return {}; }

        void unhandled_exception() { exc = std::current_exception(); }
        std::suspend_always yield_value(T value) {
            current_value = value;
            return {};
        }
        void return_void() {}
    };

    std::coroutine_handle <promise_type> coro;

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

    // 迭代器
    struct iterator {
        std::coroutine_handle <promise_type> coro;
        bool done = false;

        iterator(std::coroutine_handle <promise_type> h) : coro(h) {
            if (!coro.done()) coro.resume();
        }

        iterator& operator++() {
            if (!coro.done()) coro.resume();
            return *this;
        }

        bool operator!=(const iterator& other) const { return !done && !other.done; }
        T operator*() const { return coro.promise().current_value; }
    };

    iterator begin() { return iterator{coro}; }
    iterator end()   { return iterator{nullptr}; }
};

// 生成器函数
generator <int> int_generator(int limit) {
    for (int i = 1; i <= limit; ++i) {
        co_yield i;          // 产出值
    }
}

int main() {
    for (int n : int_generator(10)) {
        std::cout << n << ' ';
    }
    std::cout << '\n';
}

关键点解释

  1. promise_type 负责协程的生命周期。initial_suspendfinal_suspend 控制协程开始和结束时的挂起行为。
  2. yield_valueco_yield 时被调用,将当前值存入 promise 并暂停。
  3. 迭代器包装了 std::coroutine_handle,并在 operator++resume 协程。
  4. 主函数中使用范围 for 语法遍历生成器,像普通容器一样使用。

4. 协程的优势与应用场景

  • 异步 IO:协程天然适用于网络编程,co_awaitio_context 等异步操作配合可写出同步式代码。
  • 生成器:如上例,协程可轻松实现惰性序列、无限流、斐波那契数列等。
  • 状态机:在游戏编程或渲染管线中,协程可以替代繁琐的状态机逻辑。
  • 协作式多任务:通过 co_yield 实现轻量级线程切换,适用于实时系统。

5. 常见坑与调试建议

  1. 没有返回对象:确保 promise_typeget_return_object 正确返回。
  2. 悬空句柄:在异常或提前退出时,记得销毁协程句柄,防止泄漏。
  3. 多线程协程:标准库的协程并不保证线程安全,若在多线程环境使用,请使用专门的同步机制。
  4. 编译器支持:GCC、Clang、MSVC 均已实现协程,但请开启 -std=c++20 并在必要时链接 <experimental/coroutine>

6. 结语

C++20 协程是一次对语言异步能力的重构,它将复杂的回调、Future 和事件循环模式大幅简化。熟练掌握协程后,你可以写出更易读、可维护且高效的异步代码。希望本篇文章能为你迈向协程世界提供一个清晰的起点。祝编码愉快!

发表评论