C++20 协程的实现与应用

C++20 标准引入了协程(coroutine)这一强大特性,使得异步编程与生成器模式在语言层面得到了原生支持。相比传统的基于回调或线程的异步实现,协程提供了更简洁、可读性更高的代码结构。本文将从协程的内部机制、使用方式以及典型应用场景等方面进行探讨,帮助读者快速上手并掌握 C++20 协程。

1. 协程基础概念

1.1 什么是协程?

协程是一种轻量级的用户级线程,它允许在执行过程中挂起和恢复。与线程不同,协程在单线程环境下实现并发,并且切换开销极低。C++20 协程使用 co_await, co_yield, co_return 关键字来实现挂起和恢复。

1.2 协程的生命周期

  1. 创建:编译器把协程函数编译成状态机。每个挂起点生成一个 promise 对象。
  2. 开始:第一次调用协程函数会返回一个 std::coroutine_handle,协程进入暂停状态。
  3. 挂起/恢复:通过 co_awaitco_yield 触发挂起,协程暂停执行;通过外部 resume() 恢复执行。
  4. 结束:执行到 co_return 或函数尾部时,协程完成并释放资源。

2. 关键组件与实现细节

2.1 promise_type

promise_type 是协程的核心,负责管理协程的状态、返回值、异常等。标准库提供了默认实现,开发者可根据需要自定义。常见成员:

  • initial_suspend():决定协程开始时是否挂起。
  • final_suspend():决定协程结束后是否挂起。
  • get_return_object():返回协程句柄或包装类型。
  • return_value() / return_void():处理 co_return

2.2 std::suspend_alwaysstd::suspend_never

这两个结构体用于指定挂起行为:

  • std::suspend_always:始终挂起。
  • std::suspend_never:永不挂起。

2.3 状态机的生成

编译器把协程函数拆分为多个基本块,并生成跳转表。每个 co_awaitco_yield 处的代码块相当于一个状态。状态机通过内部的 promise 保存当前状态,handle 用于恢复。

3. 编写一个简单的协程

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

struct Generator {
    struct promise_type {
        std::optional <int> value;
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(int v) {
            value = v;
            return {};
        }
        Generator get_return_object() {
            return Generator{std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle <promise_type> handle;

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

    bool move_next() {
        if (!handle.done()) handle.resume();
        return !handle.done();
    }

    int current_value() const { return *handle.promise().value; }
};

Generator count_to(int n) {
    for (int i = 1; i <= n; ++i)
        co_yield i;
}

int main() {
    auto gen = count_to(5);
    while (gen.move_next()) {
        std::cout << gen.current_value() << ' ';
    }
    // 输出: 1 2 3 4 5
}

上述示例演示了一个整数生成器。关键点在于:

  • yield_value 用于产生值并挂起。
  • handle.resume() 恢复协程。
  • handle.done() 判断协程是否完成。

4. 常见应用场景

4.1 异步 I/O

协程可以与 std::future 或自定义异步库结合,实现无回调、无阻塞的 I/O。例如,使用 asio 与协程配合,代码更直观:

asio::awaitable <void> async_handler(tcp::socket socket) {
    char buffer[1024];
    std::size_t n = co_await socket.async_read_some(asio::buffer(buffer), asio::use_awaitable);
    // 处理读取的数据
}

4.2 生成器模式

协程天然支持懒加载的数据流。可用于实现链式数据处理、流式日志、分页查询等。

4.3 并发调度器

通过自定义 schedulerawaitable,可以实现轻量级任务调度。协程切换开销低,适合高并发场景。

5. 性能与注意事项

  • 栈开销:协程使用栈内状态机,局部变量保存在堆或内部缓冲区。过多挂起点可能导致巨大状态表。
  • 异常安全:若协程抛出异常,promise_type::unhandled_exception() 将被调用。建议在 promise 中捕获并转换为可恢复错误。
  • 生命周期管理:协程句柄必须在使用后显式销毁。使用 std::experimental::coroutine_handlestd::coroutine_handledestroy() 方法。

6. 进一步学习资源

  1. 《C++ Concurrency in Action》 – 提供协程与异步编程的实战案例。
  2. 官方 C++20 标准文档 – 详细描述协程实现细节。
  3. cppreference 的协程条目 – 快速参考各类协程相关类型与函数。

通过本文的介绍,读者已掌握 C++20 协程的基本概念、实现原理及典型应用。协程为现代 C++ 带来了更优雅的异步与生成器方案,值得深入学习与实践。

发表评论