C++20 协程:从概念到实践

C++20 引入了协程(coroutines)这一强大而灵活的特性,使得异步编程和生成器的实现变得更加简洁直观。协程本质上是一种能够挂起和恢复执行的函数,它通过 co_awaitco_yieldco_return 关键字实现状态的保存与恢复。下面,我们从协程的核心概念出发,结合实际案例,阐述如何在 C++20 中使用协程,并说明其优势与适用场景。

1. 协程的基本结构

一个协程函数与普通函数唯一不同的是它的返回类型必须是 std::coroutine_handlestd::futurestd::generator 等协程相关类型,或者是一个自定义类型。内部可以使用以下三种关键字:

关键字 用途 说明
co_await 等待异步操作完成 让协程挂起,直到 awaitable 对象完成
co_yield 生成值 把值返回给调用者,同时挂起协程
co_return 返回最终结果 结束协程并返回结果

协程在执行到 co_awaitco_yield 时会产生一个挂起点,调用方可以通过 std::coroutine_handle::resume() 继续执行。协程的状态会保存在协程框架中,所有局部变量都会被“暂停”而不会被销毁。

2. 协程与异步 I/O

传统的异步 I/O 需要使用回调、事件循环或 Future/Promise 组合实现。协程通过 co_await 让异步等待变得像同步代码一样直观。下面给出一个简化的网络读取示例:

#include <iostream>
#include <coroutine>
#include <future>
#include <chrono>

struct AwaitableRead {
    int socket;
    char* buffer;
    std::size_t size;
    std::chrono::steady_clock::time_point deadline;

    bool await_ready() const noexcept { return false; }

    void await_suspend(std::coroutine_handle<> h) {
        // 假设我们把读取操作注册到事件循环
        // 当数据可读时,事件循环调用 h.resume()
        std::cout << "挂起,等待数据...\n";
    }

    std::size_t await_resume() const noexcept {
        // 返回读取字节数
        std::cout << "数据已到达\n";
        return size;
    }
};

std::future<std::size_t> async_read(int socket, char* buf, std::size_t n) {
    co_return AwaitableRead{socket, buf, n, std::chrono::steady_clock::now() + std::chrono::seconds(5)};
}

int main() {
    char buffer[1024];
    auto fut = async_read(1, buffer, 1024);
    std::cout << "开始读取\n";
    auto bytes = fut.get(); // 这里会阻塞,直到协程完成
    std::cout << "读取完成,字节数: " << bytes << "\n";
}

上面代码演示了协程如何与事件循环协作,在异步 I/O 完成时自动恢复执行,避免了显式的回调层叠。

3. 协程生成器(generator)

co_yield 使得实现生成器变得轻而易举。我们可以轻松实现一个斐波那契数列生成器:

#include <iostream>
#include <coroutine>
#include <vector>

template <typename T>
struct generator {
    struct promise_type {
        T current_value;
        std::suspend_always yield_value(T value) {
            current_value = value;
            return {};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        generator get_return_object() {
            return generator{std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        void return_void() {}
        void unhandled_exception() { std::exit(1); }
    };

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

    struct iterator {
        std::coroutine_handle <promise_type> coro;
        bool operator!=(std::default_sentinel_t) const { return !coro.done(); }
        void operator++() { coro.resume(); }
        const T& operator*() const { return coro.promise().current_value; }
    };

    iterator begin() { coro.resume(); return {coro}; }
    std::default_sentinel_t end() { return {}; }
};

generator<unsigned long long> fib(unsigned int n) {
    unsigned long long a = 0, b = 1;
    for (unsigned int i = 0; i < n; ++i) {
        co_yield a;
        std::tie(a, b) = std::make_pair(b, a + b);
    }
}

int main() {
    for (auto v : fib(10)) {
        std::cout << v << ' ';
    }
}

该实现利用 co_yield 暂停协程并返回当前值,调用方通过迭代器逐个获取生成器的值。

4. 协程的优势

优势 说明
代码可读性高 异步代码写法像同步,逻辑清晰
资源管理简化 协程框架负责状态保存,减少手动管理
性能提升 通过协程调度而非线程切换,减少上下文切换成本
组合灵活 可与 std::futurestd::thread 等结合,支持多种异步模型

5. 适用场景

  1. 网络 I/O:在高并发服务器中,协程可以让每个请求保持一个轻量级状态,避免线程数膨胀。
  2. 生成器:如文件行读取、数据流处理,使用 co_yield 轻松实现惰性迭代。
  3. 游戏循环:协程可以用于实现非阻塞的脚本系统或 AI 行为树。
  4. 任务调度:在需要细粒度任务切换的实时系统中,协程提供了高效的切换机制。

6. 小结

C++20 的协程为现代 C++ 开发提供了更自然、更高效的异步编程模型。通过 co_awaitco_yieldco_return,开发者可以在保持代码可读性的同时,构建高性能的异步系统。随着标准库与第三方框架(如 Boost.Asio、cppcoro 等)的不断成熟,协程正逐步成为 C++ 生态中不可或缺的重要工具。

发表评论