C++20 协程到底是怎么实现的?

在 C++20 中引入了协程(coroutines)这一强大的语言特性,它让异步编程和延迟计算变得异常简洁。下面从实现细节、编译器支持、以及典型使用场景三方面拆解协程的工作原理。

1. 协程的核心概念

  • 协程函数:使用 co_await, co_yield, co_return 的函数,返回的类型必须是 协程返回类型(如 std::future, std::generator 等)。
  • 悬挂:在协程体内部遇到 co_awaitco_yield 时,协程会暂停执行,并把当前状态保存到协程框架中。
  • 恢复:外部通过 await_suspendoperator++(对 generator)等触发,协程从暂停点继续执行。

2. 编译器如何实现

2.1 生成隐藏的状态机

编译器把协程函数重写为一个 状态机。在编译阶段会:

  1. 为协程生成一个内部结构体,包含:
    • 需要保存的局部变量(如循环计数器、临时对象)。
    • 当前状态标记(枚举或整数)。
  2. 把原函数体拆分成若干块,每块对应一个状态,块之间通过 switch 语句跳转。

2.2 协程包装器

协程返回类型(如 std::futurestd::generator)内部持有:

  • 状态机实例。
  • 悬挂器promise_type):实现 await_ready, await_suspend, await_resume 三个接口,用于控制协程的挂起和恢复。

2.3 运行时调度

  • 协程挂起:当 co_awaitco_yield 被执行时,协程会调用 promise_type::await_suspend。如果返回 true,协程挂起;否则继续执行。
  • 调度器:协程的 await_suspend 可以接收一个自定义调度器(如 std::execution::async),决定何时恢复协程。若未提供,默认同步继续。

3. 与传统异步的区别

  • 无回调链:协程隐藏了回调的复杂性,代码像同步一样写。
  • 状态持久化:协程的所有局部变量在挂起后会被保存在堆上,避免了手动包装成 std::promise
  • 更轻量:与传统线程或事件循环相比,协程的栈开销极小。

4. 常见协程返回类型

类型 说明 用途
`std::future
` 异步操作的结果 需要线程池或异步 IO 时
`std::generator
| 生成器,支持co_yield` 流式数据、迭代器
`std::task
`(自定义) 轻量级异步任务 需要自定义调度器时

5. 示例:异步文件读取

#include <iostream>
#include <fstream>
#include <filesystem>
#include <experimental/coroutine>
#include <string>

struct async_read_file {
    struct promise_type {
        std::string buffer;
        std::experimental::suspend_always yield_value(const char* data, std::size_t len) {
            buffer.append(data, len);
            return {};
        }
        async_read_file get_return_object() {
            return async_read_file{std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        std::experimental::suspend_never initial_suspend() { return {}; }
        std::experimental::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::exit(1); }
    };

    std::coroutine_handle <promise_type> h;
    async_read_file(std::coroutine_handle <promise_type> h) : h(h) {}
    ~async_read_file() { if (h) h.destroy(); }
    std::string get() { return h.promise().buffer; }
};

async_read_file read_file(const std::filesystem::path& p) {
    std::ifstream file(p, std::ios::binary);
    const std::size_t chunk = 4096;
    char buffer[chunk];
    while (file.read(buffer, chunk) || file.gcount() > 0) {
        co_yield buffer, file.gcount();
    }
}

int main() {
    auto reader = read_file("example.txt");
    std::cout << reader.get() << std::endl;
}

该示例展示了如何用协程实现分块读取文件,并把结果拼接到字符串中。co_yield 负责把每块数据传回协程外部,编译器自动生成状态机处理挂起与恢复。

6. 小结

C++20 的协程为异步编程提供了极其优雅的语法糖。它通过编译器生成状态机、promise_type 与协程包装器实现协程的挂起与恢复。相比传统回调和线程模型,协程更易读、开销更小。掌握协程后,你可以在网络编程、游戏开发以及任何需要高并发、低延迟的场景中写出更清晰、更高效的代码。

发表评论