**C++20 中的协程:从概念到实战**

协程是 C++20 引入的一项强大功能,旨在让异步编程更接近同步代码的写法。它的核心思想是“暂停”和“恢复”,使得函数在执行过程中可以在多个点挂起,随后继续执行,而不需要手动维护状态机。下面从概念、关键特性、标准库支持以及实战示例四个维度来剖析协程。


1. 协程的基本概念

  • 暂停点(yield):协程可以在某个点把当前状态保存并返回给调用者。
  • 恢复点(resume):调用者在合适时机再次调用协程,协程会从上一次暂停点继续执行。
  • 控制流:协程在同一线程内保持同步风格,编译器会把协程拆分为内部状态机。

协程的本质是 “把函数拆分成一系列可中断的步骤”,这与传统的回调、Promise 或 Future 有着显著不同。


2. 关键语法要素

关键字 作用
co_await 等待一个 awaitable 对象(如 future、generator 等),协程在此点暂停。
co_yield 从协程产生一个值,暂停并返回给调用者。
co_return 结束协程,返回最终结果。
co_await + co_return co_yield 一样,都是暂停点,只是语义不同。

2.1 Awaitable

一个类型只要实现了 operator co_await(),并返回一个可以被 await_suspendawait_resume 处理的对象,即可成为 awaitable。

2.2 Promise 和 Handle

  • Promise:协程的状态容器,保存返回值、异常等。
  • Handle:对协程的句柄,使用 co_awaitresumedestroy 等成员函数来控制协程。

3. 标准库中的协程支持

说明
std::suspend_always 协程在 await_suspend 时始终挂起。
std::suspend_never 协程永不挂起。
`std::generator
| 简单的生成器,提供operator*()operator++()` 等迭代器语义。
`std::task
` 类似 future,代表一个可能异步完成的值。
std::async 仍然是基于 Future 的异步执行,配合协程使用可以实现更清晰的异步流程。

注意std::generatorstd::task 是 C++20 的实验性库,尚未在所有编译器中完整实现,需要使用编译器特定的 -std=gnu++20 或者开启 -fcoroutines


4. 实战示例:异步文件读取

下面给出一个简单的异步文件读取示例,演示如何使用协程实现非阻塞 I/O。

#include <iostream>
#include <coroutine>
#include <string>
#include <fstream>
#include <sstream>
#include <vector>

struct AsyncRead {
    struct promise_type;
    using handle_type = std::coroutine_handle <promise_type>;

    struct promise_type {
        std::string result;
        std::vector <char> buffer;
        std::string filePath;

        AsyncRead get_return_object() {
            return {handle_type::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        void unhandled_exception() { std::terminate(); }
        void return_void() {}
    };

    handle_type coro;
    explicit AsyncRead(handle_type h) : coro(h) {}
    ~AsyncRead() { if (coro) coro.destroy(); }
    bool await_ready() { return false; }
    void await_suspend(std::coroutine_handle<> awaiting) {
        // 这里模拟异步 I/O:在后台线程读取文件
        std::thread([=]() {
            std::ifstream in(coro.promise().filePath, std::ios::binary);
            std::ostringstream ss;
            ss << in.rdbuf();
            coro.promise().result = ss.str();
            awaiting.resume();   // 读取完毕后恢复调用者
        }).detach();
    }
    std::string await_resume() { return coro.promise().result; }
};

AsyncRead read_file_async(const std::string& path) {
    co_await std::suspend_always{}; // 允许调用者在此挂起
    std::string content = co_await AsyncRead{}; // 异步读取
    co_return content;
}

int main() {
    auto reader = read_file_async("example.txt");
    std::string data = reader.await_resume(); // 阻塞直到文件读取完成
    std::cout << "File content:\n" << data << std::endl;
}

实现细节

  1. AsyncReadawait_suspend 启动后台线程读取文件,然后在读取完成后恢复调用者。
  2. 主函数通过 await_resume() 获得读取结果。
  3. 这个示例演示了协程如何把“异步读取”抽象为类似同步的写法。

5. 协程的优势与注意事项

优势 说明
可读性 代码像同步代码,易于维护。
性能 协程的状态机通常比回调更高效,避免了堆栈分配。
可组合 多个协程可以通过 co_await 轻松串联。

注意

  • 异常传播:协程中的异常会被 promise_type::unhandled_exception 捕获,需要在 promise 中显式处理。
  • 生命周期:协程句柄必须在协程结束前保持有效,通常通过 std::future 或自定义 wrapper 管理。
  • 编译器支持:不同编译器对协程支持程度不同,务必检查编译器版本与标准选项。

6. 结语

C++20 的协程为异步编程提供了一条新途径,既保持了同步代码的直观,也充分利用了现代 CPU 的并发能力。掌握协程的基本语法与标准库工具后,你可以在网络、I/O、游戏循环等多种场景中实现更简洁、更高效的代码。祝你在协程世界里玩得开心!

发表评论