使用C++实现协程的原理与实践

协程(Coroutine)是一种轻量级的用户级线程,它可以在函数内部暂停执行并在后续恢复,从而实现异步编程、事件驱动和并发控制等功能。C++20 标准中引入了原生协程(co_awaitco_yieldco_return)支持,使得协程的使用变得更直观。本文将从原理、实现细节以及一个完整的协程例子三部分来剖析如何在 C++ 中使用协程。

一、协程的基本原理

  1. 挂起与恢复

    • 当协程执行到 co_awaitco_yieldco_return 时会“挂起”,把当前执行状态(寄存器、栈帧、局部变量等)保存下来,随后返回控制权给调用者。
    • 当协程再次被调度(通常是通过 resume())时,会恢复之前保存的状态,从挂起点继续执行。
  2. 协程句柄(std::coroutine_handle

    • 句柄是协程的管理对象,负责启动、挂起、恢复、销毁协程。
    • 通过 coroutine_handle::from_promise(promise) 可以从协程的 promise_type 获得句柄。
  3. 协程的 Promise 对象

    • 每个协程都有一个 promise_type,负责协程的生命周期管理。
    • promise_type 必须实现若干接口,如 get_return_object()initial_suspend()final_suspend()return_void()return_value() 等。

二、C++20 协程的实现细节

1. promise_type 必须实现的成员

成员函数 说明
get_return_object() 返回协程可被外部使用的对象,通常返回 `std::coroutine_handle
` 或自定义包装类型
initial_suspend() 决定协程在起始时是否立即挂起,返回 std::suspend_alwaysstd::suspend_never
final_suspend() 决定协程在完成时是否挂起,返回 std::suspend_alwaysstd::suspend_never
return_void() / return_value(T) 处理协程的返回值
unhandled_exception() 处理协程中抛出的异常
yield_value(T) 处理 co_yield 的值,返回 std::suspend_alwaysstd::suspend_never

2. co_await 与 Awaitable 对象

当协程执行 co_await expr 时,编译器会尝试将 expr 转换为 Awaitable 类型。Awaitable 必须至少实现:

成员 说明
await_ready() 若立即可完成则返回 true
await_suspend(coroutine_handle) 若需要挂起则返回 true,并保存协程句柄以供后续恢复
await_resume() 在协程恢复后返回值

3. 资源管理

协程的堆栈是由编译器自动管理的,但协程内部所分配的资源(如文件句柄、网络连接)需要在 final_suspend()return_value() 中手动释放。常用做法是将资源包装在 RAII 对象里,确保在协程结束时自动析构。

三、完整示例:异步文件读取协程

下面给出一个使用标准库实现的简单异步文件读取协程示例,演示如何使用 co_await 读取文件块并逐块处理。

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

// Awaitable:模拟异步读取
struct AsyncRead {
    std::ifstream &file;
    std::size_t size;
    std::vector <char> buffer;
    std::coroutine_handle<> caller; // 被挂起的协程句柄

    AsyncRead(std::ifstream &f, std::size_t sz)
        : file(f), size(sz), buffer(sz) {}

    bool await_ready() const noexcept { return false; }

    void await_suspend(std::coroutine_handle<> h) {
        caller = h;
        // 异步读取(这里用线程模拟)
        std::thread([this]() {
            file.read(buffer.data(), size);
            std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟延迟
            caller.resume(); // 读取完成后恢复协程
        }).detach();
    }

    std::optional<std::vector<char>> await_resume() noexcept {
        if (file.gcount() == 0) return std::nullopt;
        return buffer;
    }
};

// 协程函数:读取文件并打印
std::coroutine_handle<> read_file_async(const std::string &path) {
    std::ifstream file(path, std::ios::binary);
    if (!file) {
        std::cerr << "Failed to open file.\n";
        co_return;
    }

    while (true) {
        auto chunk_opt = co_await AsyncRead(file, 1024);
        if (!chunk_opt) break; // EOF
        const auto &chunk = *chunk_opt;
        std::cout.write(chunk.data(), file.gcount());
    }
    co_return;
}

int main() {
    auto handle = read_file_async("sample.bin");
    handle.resume(); // 开始执行
    // 在此可做其他工作,协程会在后台异步完成
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 0;
}

代码说明

  1. AsyncRead

    • await_ready() 始终返回 false,表示需要挂起。
    • await_suspend() 启动一个新线程模拟异步读取,并在读取完成后调用 caller.resume()
    • await_resume() 将读取到的数据返回给协程。
  2. read_file_async

    • 通过 co_await AsyncRead 挂起等待文件块,读取完成后继续。
    • co_return 在文件结束后终止协程。
  3. 主函数

    • 调用 read_file_async 得到协程句柄并立即 resume(),随后主线程可以继续执行其他任务。

四、总结

  • C++20 原生协程为异步编程提供了更轻量、更易读的实现方式。
  • 关键是理解协程的挂起/恢复、promise_type、以及 Awaitable 的三个接口。
  • 在实际项目中,可以将协程与 I/O 框架(如 Boost.Asio、libuv)结合,进一步提升性能。

通过上述原理解析与完整示例,相信你已经可以在自己的 C++ 项目中自由使用协程,实现更高效、可读性更好的异步代码。

发表评论