C++20协程的原理与实践

C++20 的协程(coroutine)是一项强大的语言特性,它让异步编程变得像同步编程一样直观。协程通过语言级别的支持实现了暂停、恢复和返回值的概念,极大简化了基于事件驱动、网络 IO、UI 更新等场景的代码。本文将从协程的核心概念、实现原理以及一个实用的文件读取示例三方面,系统阐述 C++20 协程的实用价值。

1. 协程的核心概念

  1. 协程句柄(std::coroutine_handle
    协程句柄是协程与外部世界交互的桥梁。它可以用来挂起、恢复或销毁协程。句柄内部持有协程状态机的入口地址与上下文信息。

  2. 协程返回类型(std::suspend_always / std::suspend_never
    这两个类型告诉编译器协程在何时挂起。suspend_always 在每一次 co_await 处暂停,suspend_never 则不暂停。

  3. co_awaitco_yieldco_return

    • co_await 用于等待一个可等待对象,编译器会把它拆分成 await_ready, await_suspend, await_resume 三个阶段。
    • co_yield 用于生成值,适用于实现生成器。
    • co_return 用于返回协程最终值并结束协程。

2. 协程的实现原理

C++ 协程的实现可视为一个隐式的状态机。编译器会把协程函数拆分成若干状态块,并在 co_awaitco_yield 处生成跳转点。

  • 栈展开:协程的局部变量在堆上分配,避免了堆栈不够时的栈溢出问题。
  • Promise 对象:每个协程都有一个 promise_type,用于存放协程返回值、异常信息以及状态机的入口。编译器在协程进入和退出时自动调用 get_return_object, initial_suspend, final_suspend, return_value, unhandled_exception 等函数。
  • 协程帧(Coroutine Frame):是一块在堆上分配的内存块,存放协程的栈帧、promise 对象以及其他必要信息。

3. 典型使用场景

  1. 异步 IO:利用 co_await 等待事件完成,避免回调地狱。
  2. 生成器:通过 co_yield 实现惰性序列。
  3. 协程调度器:结合事件循环,实现协程切换与调度。

4. 实战示例:异步读取文件

下面给出一个使用 C++20 协程实现异步文件读取的完整示例。该示例演示了如何将标准文件 IO 适配为可等待对象,并在主程序中使用 co_await 简洁地读取文件。

#include <iostream>
#include <coroutine>
#include <string>
#include <vector>
#include <thread>
#include <chrono>
#include <fstream>
#include <sstream>
#include <mutex>
#include <condition_variable>

// ---------- 1. 可等待对象 AsyncFile ----------

struct AsyncFile {
    struct promise_type {
        std::string data;
        std::exception_ptr ex;
        std::condition_variable cv;
        std::mutex mtx;
        bool ready{false};

        AsyncFile get_return_object() {
            return AsyncFile{std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { ex = std::current_exception(); }
        template<class T>
        void return_value(T&& value) { data = std::forward <T>(value); }
    };

    std::coroutine_handle <promise_type> coro;

    AsyncFile(std::coroutine_handle <promise_type> h) : coro(h) {}
    ~AsyncFile() { if (coro) coro.destroy(); }

    // 让调用者等待文件读取完成
    std::string await_resume() {
        std::unique_lock<std::mutex> lk(coro.promise().mtx);
        coro.promise().cv.wait(lk, [&] { return coro.promise().ready; });
        if (coro.promise().ex) std::rethrow_exception(coro.promise().ex);
        return coro.promise().data;
    }
};

AsyncFile read_file_async(const std::string& path) {
    try {
        // 模拟耗时 IO
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::ifstream ifs(path, std::ios::binary);
        if (!ifs) throw std::runtime_error("文件打开失败");
        std::stringstream buffer;
        buffer << ifs.rdbuf();
        // 通知等待者
        auto& prom = std::coroutine_handle<AsyncFile::promise_type>::from_promise(*ifs);
        prom.promise().ready = true;
        prom.promise().cv.notify_all();
        co_return buffer.str();
    } catch (...) {
        std::coroutine_handle<AsyncFile::promise_type>::from_promise(*ifs).promise().ex = std::current_exception();
        co_return std::string{};
    }
}

// ---------- 2. 主协程 ----------

struct MainTask {
    struct promise_type {
        std::coroutine_handle <promise_type> coro;
        MainTask get_return_object() { return {coro}; }
        std::suspend_never initial_suspend() noexcept { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle <promise_type> coro;
    MainTask(std::coroutine_handle <promise_type> h) : coro(h) {}
    ~MainTask() { if (coro) coro.destroy(); }
};

MainTask main_task() {
    std::cout << "开始读取文件...\n";
    std::string content = co_await read_file_async("example.txt");
    std::cout << "文件内容已读取,长度为:" << content.size() << " 字节\n";
}

int main() {
    auto task = main_task();
    // 这里简单等待任务结束
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 0;
}

代码说明

  1. AsyncFile:包装了协程的 promise,内部通过条件变量与 std::mutex 来同步读取完成。
  2. read_file_async:示例中的异步读取逻辑使用 std::this_thread::sleep_for 模拟 IO 延迟。真实项目中可以使用 ASIO、WinIO 等库实现真正的异步 IO。
  3. main_task:演示了如何在主协程中使用 co_await 等待文件读取完成,并处理返回值。

5. 小结

C++20 协程通过语言层面的支持,彻底改变了传统同步/异步编程的面貌。它不需要额外的回调或状态机手写,能够让复杂的异步流程写成像同步一样直观的代码。

  • 优势:代码简洁、易维护、性能可控。
  • 局限:需要编译器支持(至少 C++20)并可能导致堆分配开销。
  • 未来:随着协程调度器、事件循环库的成熟,C++ 的异步编程将进一步完善。

希望本文能帮助你快速上手 C++20 协程,并在项目中发挥其强大优势。

发表评论