C++中的协程(Coroutine)实现原理与实践

在C++20引入协程之后,协程成为了一个极具吸引力的异步编程工具。它不仅让异步代码像同步代码一样直观,而且在性能上往往优于传统的回调或基于线程的实现。本文从协程的底层实现原理出发,结合实际代码示例,帮助读者快速掌握协程的使用方法和常见陷阱。

1. 协程基本概念

协程是一种轻量级的用户级线程,能够在任意位置挂起(co_awaitco_yieldco_return)并在以后恢复执行。与传统线程不同,协程的上下文切换只涉及寄存器、栈指针等少量状态,几乎不需要内存拷贝,开销非常小。

2. 协程的三大组件

  1. Promise
    • 用来在协程开始时准备状态,并在协程结束时返回结果。promise_type 是每个协程必须定义的类,编译器会自动使用它来创建和销毁协程句柄。
  2. Coroutine Handle
    • `std::coroutine_handle `,负责管理协程的生命周期,提供 `resume()`、`destroy()`、`done()` 等操作。
  3. Suspension Points
    • co_awaitco_yieldco_return 引入,决定协程何时挂起。

3. 协程的执行流程

  1. 编译器在遇到 co_await 时,将当前函数拆分为若干个“帧”。
  2. 每个帧对应一段代码,帧之间的状态保存在 promise_type 对象中。
  3. co_await 语句会调用被 await 的对象的 await_ready()await_suspend()await_resume()
  4. await_ready() 返回 false,则执行 await_suspend(),此时协程挂起,调用者可以决定何时恢复。
  5. 当外部调用 handle.resume() 时,协程恢复执行,直至下一个挂起点或结束。

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

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

namespace fs = std::filesystem;

// 1. awaitable 类型:异步文件读取
struct AsyncFileRead {
    std::string path;
    std::vector <char> buffer;
    std::size_t offset = 0;

    bool await_ready() const noexcept { return false; }

    // 当协程挂起时将继续的函数包装进一个异步任务
    void await_suspend(std::coroutine_handle<> h) const noexcept {
        // 简化实现:直接使用同步读取,随后恢复协程
        std::ifstream file(path, std::ios::binary);
        if (file) {
            file.seekg(0, std::ios::end);
            std::size_t size = file.tellg();
            buffer.resize(size);
            file.seekg(0, std::ios::beg);
            file.read(buffer.data(), size);
        }
        h.resume(); // 立即恢复
    }

    std::vector <char> await_resume() noexcept { return std::move(buffer); }
};

// 2. Promise 结构
struct FileReaderPromise {
    std::vector <char> result;

    auto get_return_object() {
        return std::coroutine_handle <FileReaderPromise>::from_promise(*this);
    }
    std::suspend_never initial_suspend() { return {}; }
    std::suspend_never final_suspend() noexcept { return {}; }
    void unhandled_exception() { std::terminate(); }
    void return_value(std::vector <char> r) { result = std::move(r); }
};

using FileReaderTask = std::coroutine_handle <FileReaderPromise>;

// 3. 协程函数
FileReaderTask read_file(const std::string& path) {
    AsyncFileRead awaitable{path};
    co_return co_await awaitable;
}

// 4. 主函数
int main() {
    auto handle = read_file("example.txt");
    handle.resume(); // 触发文件读取
    std::vector <char> data = std::move(handle.promise().result);
    std::cout << "读取到 " << data.size() << " 字节\n";
    handle.destroy();
}

说明

  • 这里的 AsyncFileRead::await_suspend 采用同步读取,并立即恢复协程。实际应用中可以将 I/O 操作委托给线程池或平台异步 API。
  • FileReaderTask 返回的句柄允许外部控制协程的挂起、恢复和销毁。

5. 常见陷阱与最佳实践

陷阱 说明 解决方案
对象生命周期 co_await 的 awaitable 必须在协程生命周期内保持有效 让 awaitable 通过值或引用持有在 promise 内部
悬空协程句柄 错误地使用 handle.resume() 后忘记 destroy() 推荐使用 std::unique_ptr 或 RAII 包装器
阻塞主线程 await_suspend 内部同步阻塞会导致协程挂起后仍然阻塞 通过异步 I/O 或线程池实现真正的非阻塞
异常传播 协程内的异常不自动捕获 在 promise 的 unhandled_exception 里处理或使用 co_return

6. 与传统异步模型对比

特性 传统回调 std::async 协程
可读性
资源占用 高(线程) 低(线程池) 极低
错误处理 复杂 简单 与同步代码同样简洁
适用场景 小型异步任务 大量并行计算 IO 密集型、事件驱动

7. 结语

协程为 C++ 程序员提供了一种既直观又高效的异步编程方式。只要掌握好 promise_type、协程句柄以及 awaitable 的三种接口,几乎可以将所有异步任务转化为同步样式的代码。随着标准库不断完善,协程的生态将愈发成熟,值得每位 C++ 开发者投入时间学习与实践。

发表评论