C++ 20 中的协程:从设计到实践

在 C++20 里,协程(Coroutine)成为了一项强大而灵活的特性,极大地简化了异步编程和生成器的实现。下面从设计原理、核心概念、标准库支持以及实际编码实例几个角度,逐步剖析 C++ 协程的细节,帮助你快速上手并在项目中有效利用。

一、协程的设计哲学

协程可以被看作是“轻量级线程”,它们支持暂停(co_awaitco_yield)与恢复,线程切换的开销极低。C++ 协程的设计核心是:

  1. 非阻塞:协程在等待某个异步事件时,能够挂起执行,让调用方继续执行其他任务,避免阻塞线程。
  2. 无状态切换:协程的状态保存在栈帧之外(通常在堆上分配),实现时采用状态机技术。
  3. 透明的语法:通过 co_awaitco_yield 等关键字,协程的写法与普通同步代码几乎无差别。

二、核心概念与实现细节

1. 协程句柄(std::coroutine_handle

协程句柄是对协程本体的引用,负责控制协程的生命周期。它可以:

  • resume():恢复协程执行。
  • destroy():释放协程资源。
  • done():检查协程是否已完成。

2. 协程 Promise

Promise 是协程内部的数据容器,包含协程的返回值、异常信息以及 await_transform 等成员。协程函数在编译时会被转换成一个返回 promise_type 的结构体。常见成员:

  • get_return_object():返回协程句柄或其他封装对象。
  • initial_suspend():协程开始时是否立即挂起。
  • final_suspend():协程结束时的挂起点,通常需要 co_await std::suspend_always{}
  • return_value() / return_void():处理 co_return

3. Awaitable 对象

任何满足以下特性的类型都可以被 co_await

  • await_ready():返回 true 则立即继续执行,否则挂起。
  • await_suspend(coroutine_handle):挂起时调用,传入当前协程句柄。
  • await_resume():挂起后恢复执行时调用,返回值作为 co_await 的结果。

三、标准库支持

C++20 标准库提供了若干与协程相关的工具:

组件 说明
std::suspend_always 始终挂起的 Awaitable,常用于 initial_suspendfinal_suspend
std::suspend_never 永不挂起,常用于快速启动协程。
`std::generator
` 用于生成器模式,内部实现为协程。
`std::task
| 异步任务包装器,支持co_await`。
std::future / std::promise 与协程可配合使用,实现异步结果获取。

四、实战案例:异步文件读取

下面给出一个完整的示例,演示如何使用协程进行异步文件读取,结合 std::generator 逐行返回文件内容。

#include <coroutine>
#include <iostream>
#include <fstream>
#include <string>
#include <optional>
#include <filesystem>

namespace fs = std::filesystem;

// 简单的异步读取行协程
struct async_line_reader {
    struct promise_type {
        std::optional<std::string> current_line;
        std::string file_path;

        async_line_reader get_return_object() {
            return async_line_reader{
                std::coroutine_handle <promise_type>::from_promise(*this)
            };
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        void unhandled_exception() {
            std::exit(1); // 简化错误处理
        }

        void return_void() {}
    };

    std::coroutine_handle <promise_type> coro;
    bool done = false;

    async_line_reader(std::coroutine_handle <promise_type> h)
        : coro(h) {}

    ~async_line_reader() { if (coro) coro.destroy(); }

    // 逐行获取
    std::optional<std::string> next() {
        if (!coro || coro.done()) { done = true; return std::nullopt; }
        coro.resume();
        if (coro.done()) { done = true; return std::nullopt; }
        return coro.promise().current_line;
    }
};

// 协程体:读取文件行
async_line_reader read_file_lines(std::string path) {
    std::ifstream ifs(path);
    if (!ifs.is_open())
        co_return;

    std::string line;
    while (std::getline(ifs, line)) {
        co_yield line; // co_yield 会挂起并返回当前行
    }
}

int main() {
    const std::string path = "example.txt";
    if (!fs::exists(path)) {
        std::ofstream ofs(path);
        ofs << "Hello\n";
        ofs << "C++20\n";
        ofs << "Coroutines\n";
    }

    async_line_reader reader = read_file_lines(path);
    while (auto line_opt = reader.next()) {
        std::cout << *line_opt << std::endl;
    }

    return 0;
}

代码解读

  1. async_line_reader 包装了协程句柄,并提供 next() 接口逐行读取。
  2. read_file_lines 作为协程函数,使用 co_yield 暂停并返回当前行。
  3. 主函数中通过循环调用 next(),实现异步文件读取的效果。

五、协程的性能与注意事项

  • 堆分配:协程体的状态通常在堆上分配,频繁创建协程可能导致内存碎片。可以考虑使用对象池或自定义分配器。
  • 异常安全:协程 Promise 的 unhandled_exception 应妥善处理异常,防止泄漏。
  • 可读性:虽然协程语法简洁,但过度嵌套的 co_await 可能导致可读性下降。建议保持层次清晰。

六、总结

C++20 协程为异步编程带来了巨大的便利,从生成器到网络 IO,都可以用更直观的语法实现。掌握 Promise、Awaitable、协程句柄的核心概念,以及标准库提供的工具,你就能在项目中写出既高效又可维护的异步代码。祝你编码愉快,玩转 C++ 协程!

发表评论