**C++20协程(Coroutines)如何工作?**

C++20 引入了协程(coroutines)这一强大的语言特性,极大地简化了异步编程、生成器以及惰性求值等场景的实现。下面从概念、实现细节以及常见使用案例几个角度,详细剖析协程的工作原理。


1. 协程概念回顾

  • 协程:在运行过程中能够挂起(suspend)和恢复(resume)的函数。它们的执行状态被保留,能够在不同时间点间断执行。
  • 关键字co_awaitco_yieldco_return,以及协程返回类型 std::coroutine_handle
  • 目标:把异步或惰性计算的流程拆分成若干个挂起点,让调用者可以像同步代码一样书写。

2. 代码结构与关键字

#include <coroutine>
#include <iostream>

struct Task {
    struct promise_type {
        Task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};

Task example() {
    std::cout << "Start\n";
    co_await std::suspend_always{};      // 挂起点 1
    std::cout << "Resume\n";
    co_return;                           // 结束
}
  • promise_type:每个协程都有对应的 promise 对象,用来管理协程的生命周期和返回值。
  • initial_suspend / final_suspend:分别决定协程在开始和结束时是否挂起。
  • co_await / co_yield / co_return:在协程内部的挂起点。

3. 协程底层实现(简化版)

  1. 生成器栈
    C++ 编译器在编译协程时会把函数体拆分成若干个基本块,并在栈上为每个挂起点保存局部变量的快照(称为“状态机”)。

  2. 状态机
    编译器将协程视作一个有限状态机(FSM)。每个挂起点对应一个状态,执行到挂起点时会把当前状态保存在协程句柄中,随后返回控制权。

  3. 协程句柄
    `std::coroutine_handle

    ` 保存了协程状态、返回地址和 promise 对象。通过 `handle.resume()` 可以恢复协程。
  4. 内存管理
    协程对象本身不持有堆内存,所有局部变量都保存在堆上(由协程句柄管理)。当协程完成时,final_suspendsuspend_always 触发后,资源被释放。


4. 常见使用场景

场景 典型实现 优点
异步 I/O co_await asyncRead() 代码可读性高,回调链消失
生成器 co_yield value 惰性迭代,内存占用小
管道/流 co_yield 组合 直观的流水线处理
协程化线程 co_spawn + awaitable 更细粒度调度

5. 一个完整的异步文件读取示例

#include <coroutine>
#include <iostream>
#include <fstream>
#include <string>
#include <future>

struct AwaitableRead {
    std::ifstream& stream;
    char buffer[1024];
    std::size_t nread;

    AwaitableRead(std::ifstream& s) : stream(s) {}

    bool await_ready() const noexcept { return false; }

    void await_suspend(std::coroutine_handle<> h) noexcept {
        // 简单模拟异步,实际应使用事件驱动或线程池
        std::async(std::launch::async, [this, h]() mutable {
            stream.read(buffer, sizeof(buffer));
            nread = stream.gcount();
            h.resume();
        });
    }

    std::size_t await_resume() const noexcept { return nread; }
};

struct AsyncFileReader {
    std::ifstream file;

    AsyncFileReader(const std::string& path) : file(path, std::ios::binary) {}

    std::future<std::size_t> readChunk() {
        co_return co_await AwaitableRead(file);
    }
};

int main() {
    AsyncFileReader reader("example.bin");
    auto future = reader.readChunk();
    std::size_t bytes = future.get();
    std::cout << "Read " << bytes << " bytes.\n";
}
  • 通过自定义 AwaitableRead,我们把同步读取包装成协程可等待对象,内部使用 std::async 模拟异步行为。
  • 调用方使用 co_awaitstd::future 搭配,保持了同步语义。

6. 性能与陷阱

  • 开销:协程的状态机、堆分配和上下文切换会带来一定成本。对极小粒度操作建议使用回调或同步方式。
  • 异常安全:如果协程中抛出异常,promise_type::unhandled_exception 会被调用,需要自行决定是否将异常抛出给外层。
  • 内存泄漏:协程句柄忘记销毁或 final_suspend 未返回 suspend_always 可能导致资源泄漏。

7. 结语

C++20 协程为语言层面提供了强大的异步控制流能力。掌握其基本概念、编译器生成的状态机以及常见使用模式,可以让你在高性能计算、网络编程和数据流处理等领域书写更简洁、更易维护的代码。下一步可以尝试结合 std::experimental::generator 或第三方库(如 asio)深入学习协程的实际应用。

发表评论