**C++20 协程:让异步编程像同步一样简洁**

在 C++20 中,协程(Coroutines)被正式引入,使得异步操作不再需要回调链或手动状态机。通过 co_awaitco_yieldco_return,开发者可以以同步的思维写出真正异步、非阻塞的代码。本文从协程的基本原理开始,介绍常见的协程类型,给出完整的异步文件读取示例,并讨论与传统 std::future 的差异与适用场景。


1. 协程的基本概念

协程是一种可挂起(suspend)和恢复(resume)的函数。与普通函数不同,协程在执行过程中可以在某个点暂停,随后在需要时继续执行,而不必在调用点等待完成。C++ 的协程实现依赖于以下三个关键关键字:

  • co_await:在协程中等待一个 awaitable 对象,挂起当前协程直到该对象完成。
  • co_yield:产生一个值,并挂起协程,等待下次获取。
  • co_return:结束协程并返回最终值。

协程的底层实现利用 协程句柄std::coroutine_handle)和 协程 promisepromise_type)来管理状态。编译器会为每个协程生成一个隐藏的状态机,负责保存局部变量、控制流以及挂起点。


2. 协程的三种主要形式

类型 关键字 典型用法
异步函数 co_await `awaitable
foo();`
生成器 co_yield `generator
seq();`
协程返回值 co_return `Task
async_task();`
  • 异步函数:返回一个 awaitable,调用方使用 co_await 等待结果。适合网络 IO、磁盘 IO 等 I/O 密集型任务。
  • 生成器:类似 C# 的 yield return,一次返回一个值,常用于遍历序列。返回类型通常是 `generator ` 或自定义类型。
  • 协程返回值:类似于 std::future,但更轻量且不需要线程池。适用于非阻塞计算。

3. 经典示例:异步读取文件

下面给出一个完整的异步文件读取实现,演示如何将标准文件 I/O 包装成 awaitable,并在主协程中使用 co_await

#include <coroutine>
#include <iostream>
#include <vector>
#include <fstream>
#include <string>
#include <system_error>
#include <thread>
#include <chrono>
#include <optional>

// 1. Awaitable 结构体
struct AsyncRead {
    std::string filename;
    std::vector <char> buffer;
    std::optional<std::error_code> ec; // 读取错误
    struct promise_type {
        AsyncRead get_return_object() { return {nullptr, std::vector <char>(), std::nullopt}; }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    using handle_type = std::coroutine_handle <promise_type>;

    // 让协程可以被 await
    struct awaiter {
        AsyncRead &ar;
        bool await_ready() noexcept { return false; }
        void await_suspend(handle_type h) noexcept {
            std::thread([ar = ar, h]() mutable {
                try {
                    std::ifstream ifs(ar.filename, std::ios::binary);
                    if (!ifs) throw std::system_error(errno, std::generic_category(), "open file");
                    ar.buffer.assign((std::istreambuf_iterator <char>(ifs)),
                                     std::istreambuf_iterator <char>());
                } catch (...) {
                    ar.ec = std::make_optional(std::error_code(errno, std::generic_category()));
                }
                h.resume(); // 读取完成后恢复协程
            }).detach();
        }
        std::vector <char> await_resume() noexcept {
            if (ar.ec) throw std::system_error(*ar.ec);
            return std::move(ar.buffer);
        }
    };

    awaiter operator co_await() { return { *this }; }
};

// 2. 异步读取函数
AsyncRead read_file_async(const std::string &name) {
    co_return std::move(name);
}

// 3. 主协程
struct Run {
    struct promise_type {
        Run get_return_object() { return {}; }
        std::suspend_never initial_suspend() noexcept { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};

Run main_coroutine() {
    try {
        std::vector <char> data = co_await read_file_async("example.txt");
        std::cout << "文件大小: " << data.size() << " 字节\n";
    } catch (const std::system_error &e) {
        std::cerr << "读取失败: " << e.what() << '\n';
    }
}

int main() {
    main_coroutine(); // 直接执行主协程
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待异步读取完成
    return 0;
}

说明:

  1. AsyncRead 是一个 awaitable,内部使用 std::thread 异步读取文件。实际生产环境建议使用专门的异步 I/O 库(如 libuv、asio)来避免创建大量线程。
  2. co_await read_file_async 让主协程挂起,等到 AsyncRead 完成后恢复。
  3. main_coroutine 通过 co_return 结束,示例中我们把协程作为普通函数直接调用。

4. 与 std::future 的比较

特点 std::future C++20 协程
执行模型 通常绑定到线程池或同步任务 线程无关、可挂起
错误传播 通过异常或 future::get() 通过 await_resume() 抛出
性能 需要上下文切换、对象拷贝 仅在挂起点产生状态机,开销极小
使用场景 简单异步结果获取 复杂异步流程、需要多次挂起、生成器等

协程在处理大量 I/O 时可以显著降低资源消耗,尤其在需要高并发时,它们的“非阻塞挂起”特性使得单线程事件循环也能并行执行。


5. 实践建议

  1. 不要滥用线程:协程本身不产生线程,只是编译器生成状态机。真正的异步 I/O 必须依赖 OS 提供的事件驱动或第三方库。
  2. 使用 co_await 时注意对象生命周期:协程句柄会在协程结束时销毁,确保 awaitable 的内部资源不会悬挂。
  3. 与现有 std::future 混合:可以将协程包装成 std::future,或在协程里 co_await std::async
  4. 学习 generator:用于生成大数据序列时,比一次性生成整份数据更节省内存。

6. 结语

C++20 的协程为语言注入了一种全新的异步表达方式,让复杂的异步逻辑变得像同步代码一样直观。掌握协程后,你可以:

  • co_await 编写事件驱动网络服务器;
  • co_yield 创建高效的流式处理管道;
  • co_return 简化异步任务的返回。

未来的标准可能会进一步扩展协程的功能(如 await_transformtask 类型)。现在就去试试上述示例,感受一下“协程编程”带来的新风貌吧!

发表评论