**什么是 C++20 协程(coroutine)以及如何使用它来简化异步编程?**

C++20 在标准库中正式引入了协程(coroutine)这一强大的语法和运行时特性。它的核心目标是让我们可以在保持同步代码可读性的前提下,轻松实现异步、惰性计算或状态机等复杂控制流。下面从概念、实现细节和实战示例三部分,系统地讲解协程的使用方式。


1. 协程的基本概念

  1. 协程与线程的区别

    • 线程是操作系统级别的并发单元,切换成本高,且每个线程需要单独的栈空间。
    • 协程是语言级别的轻量级并发,多个协程共用同一线程的栈空间,切换只需保存和恢复程序计数器(即“挂起点”)即可。
  2. 协程的生命周期

    • 创建:调用协程函数时,编译器会把函数拆成一个状态机。
    • 挂起(co_awaitco_yieldco_return:协程在执行到这些关键字时会暂存状态并返回控制给调用者。
    • 恢复:调用者通过 resume 重新激活协程,恢复到上一次挂起的位置继续执行。
  3. 协程返回类型

    • 协程函数的返回类型必须满足 std::experimental::coroutine_traits 或自定义的协程返回类型。
    • 标准库提供了 std::future, std::generator, std::task(可用在 C++23)等。

2. C++20 协程的实现细节

  1. 关键字

    • co_await:等待一个 awaitable 对象完成。
    • co_yield:向外部“产生”一个值,暂停执行。
    • co_return:结束协程并返回结果。
  2. awaitable 对象
    一个对象若要被 co_await,必须实现以下成员:

    bool await_ready();          // 是否立即完成
    void await_suspend(std::coroutine_handle<> h); // 若未完成,挂起协程
    auto await_resume();         // 结果返回

    常见实现:std::future, std::promise, 自定义异步 I/O 对象等。

  3. 协程句柄(std::coroutine_handle
    句柄是协程的引用类型,用来控制协程的挂起、恢复与销毁。

  4. **生成器(`std::generator

    `)** 通过 `co_yield` 产生序列值,调用者使用 `begin()/end()` 迭代。

3. 简易示例:异步读取文件

下面用标准库(C++20)编写一个异步读取文件的协程示例,演示如何结合 std::filesystemstd::async

#include <iostream>
#include <coroutine>
#include <vector>
#include <fstream>
#include <sstream>
#include <future>
#include <chrono>

// 1. awaitable: 异步文件读取
struct async_read
{
    std::string path;
    std::vector <char> buffer;
    std::coroutine_handle<> h;          // 协程句柄,用于挂起与恢复

    async_read(std::string p) : path(std::move(p)) {}

    bool await_ready() noexcept { return false; }           // 总是挂起
    void await_suspend(std::coroutine_handle<> coro) noexcept
    {
        h = coro;
        // 异步执行读取
        std::async(std::launch::async, [this]{
            std::ifstream file(path, std::ios::binary | std::ios::ate);
            if (!file) { std::cerr << "Open file failed\n"; return; }
            auto size = file.tellg();
            file.seekg(0);
            buffer.resize(static_cast <size_t>(size));
            file.read(buffer.data(), size);
            // 读取完成后恢复协程
            h.resume();
        });
    }
    std::vector <char> await_resume() noexcept { return std::move(buffer); }
};

// 2. 协程返回类型
struct task
{
    struct promise_type
    {
        task 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(); }
    };
};

// 3. 协程函数
task read_file_async(std::string path)
{
    auto data = co_await async_read{std::move(path)};
    std::cout << "File size: " << data.size() << " bytes\n";
    std::string content(data.begin(), data.end());
    std::cout << "Content preview: " << content.substr(0, 50) << "...\n";
}

int main()
{
    read_file_async("example.txt");
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待异步完成
    return 0;
}

说明

  • async_read 是一个自定义 awaitable,负责异步打开文件并读取内容。
  • read_file_async 协程使用 co_await 等待 async_read 完成后获取数据。
  • task 是最小化的协程返回类型,用来让 read_file_async 成为协程。

4. 协程在实际项目中的常见场景

场景 协程作用
网络 I/O 通过 co_await 等待网络套接字完成读/写,代码保持同步风格
生成器 std::generator 用于遍历大集合(如遍历文件行、数据库结果集)
状态机 co_yield 产生多阶段过程,适合编写复杂协议解析
任务调度 与事件循环结合,实现微线程、协程池等轻量级并发框架

5. 小结

C++20 协程提供了一套语法糖和运行时机制,让我们可以在保持代码可读性的同时,实现高效的异步、惰性或状态机程序。理解其关键字、awaitable 的实现和协程句柄的使用,是上手协程的前提。随着 C++23 引入 std::taskstd::promise 的进一步改进,协程将成为 C++ 高性能编程的重要工具。


后续学习建议

  1. 阅读 RFC 2600(C++20 协程)了解实现细节。
  2. 结合 boost::asiolibuv 等网络库,实践异步网络编程。
  3. 探索协程与 std::future 的混合使用,构建更复杂的并发框架。

发表评论