探究C++的协程(Coroutines)实现原理与实战技巧

协程是 C++20 引入的一项重要特性,它通过 co_awaitco_yieldco_return 三个关键字,使得函数可以在执行过程中暂停与恢复,从而实现轻量级的异步编程。相比传统的线程、回调和 promise/async,协程具有更低的栈占用、更清晰的业务逻辑写法以及更优的性能。本文将从实现原理、关键接口、内存模型以及实际应用场景四个维度,深入剖析 C++ 协程,并给出一份完整的实战示例。

1. 协程的底层实现

C++ 协程的实现核心是 状态机。当编译器遇到带 co_await/co_yield/co_return 的函数时,会把它拆解成一个隐式生成的类 promise_type(承诺类型)和一个 生成器(coroutine handle)。该类包含所有局部变量的拷贝/移动语义以及 initial_suspend()final_suspend() 等生命周期钩子。

struct MyPromise {
    int value_;
    auto get_return_object() { return std::coroutine_handle <MyPromise>::from_promise(*this); }
    std::suspend_always initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void unhandled_exception() { std::terminate(); }
    void return_value(int v) { value_ = v; }
};

当协程执行到 co_await 时,编译器会把控制权交给 awaiter 对象,awaiter 必须实现 await_ready()await_suspend()await_resume() 三个成员函数。await_suspend 返回 true 时协程被挂起,false 则立即继续。

协程的栈由 resume 栈帧栈 组成:

  • 帧栈(Frame)记录协程内部的局部变量以及 promise_type 对象。
  • resume 栈 存储协程的返回地址,类似于普通函数的返回栈。

因为帧栈被编译器在栈空间之外(通常是堆)分配,所以协程可以在任何调用层级被挂起,甚至跨线程恢复。

2. 关键接口与语义

关键字 说明 典型用法
co_await 等待一个 awaiter,返回 awaiter 的 await_resume() 结果 auto result = co_await asyncOperation();
co_yield 产生一个值并挂起协程 co_yield i;
co_return 结束协程并返回值 co_return 42;

std::future 与协程配合使用时,常见的实现是 std::future::operator co_await。C++20 通过 std::experimental::coroutine_traits 为自定义 awaiter 提供适配接口,允许把任意对象转成 awaiter。

3. 内存模型与异常传播

协程的异常传播与普通函数类似,异常会在 await_suspend 或者 co_return 处被捕获,并交给 promise_type::unhandled_exception() 处理。若你想在协程内部捕获异常,可以直接使用 try/catch 包围 co_await

协程帧中保存的对象会遵守 RAII 原则,异常导致的堆栈展开会自动析构。值得注意的是,promise_type 必须是 noexcept 的,除非你手动在 unhandled_exception() 里做异常处理。

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

下面给出一个简易的异步文件读取协程,演示如何结合 std::ifstreamco_awaitstd::future

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

struct AsyncReadAwaiter {
    std::string filename_;
    std::string result_;
    std::coroutine_handle<> handle_;

    AsyncReadAwaiter(const std::string& f, std::coroutine_handle<> h)
        : filename_(f), handle_(h) {}

    bool await_ready() const noexcept { return false; }

    void await_suspend(std::coroutine_handle<> h) noexcept {
        std::thread([this, h]{
            std::ifstream in(filename_);
            if (in) {
                std::ostringstream ss;
                ss << in.rdbuf();
                result_ = ss.str();
            }
            h.resume(); // 恢复协程
        }).detach();
    }

    const std::string& await_resume() const noexcept { return result_; }
};

std::future<std::string> asyncReadFile(const std::string& file) {
    struct Awaiter : AsyncReadAwaiter {
        Awaiter(const std::string& f, std::coroutine_handle<> h)
            : AsyncReadAwaiter(f, h) {}
    };

    struct AwaiterPromise {
        std::promise<std::string> prom_;
        Awaiter get_return_object() { return {prom_.get_future(), std::coroutine_handle<>::from_promise(*this)}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { prom_.set_exception(std::current_exception()); }
        void return_value(std::string v) { prom_.set_value(std::move(v)); }
    };

    struct AwaiterCoro {
        AwaiterCoro(Awaiter&& a) : a_(std::move(a)) {}
        Awaiter a_;
        auto operator co_await() const noexcept { return a_; }
    };

    struct Coroutine {
        std::coroutine_handle <AwaiterPromise> handle_;
        std::future<std::string> get() { return handle_.promise().prom_.get_future(); }
    };

    auto coro = []() -> AwaiterCoro {
        std::string content = co_await Awaiter(file, std::coroutine_handle<>::from_promise(*this));
        co_return std::move(content);
    }();

    return coro.get();
}

int main() {
    auto fut = asyncReadFile("sample.txt");
    std::cout << "File content:\n" << fut.get() << std::endl;
    return 0;
}

说明

  • AsyncReadAwaiter 在后台线程中读取文件,然后恢复协程。
  • asyncReadFile 返回一个 std::future<std::string>,主线程可继续执行。
  • 该示例演示了协程与线程、std::future 的互操作,充分利用协程的非阻塞特性。

5. 性能与最佳实践

方面 建议
堆栈开销 对于频繁创建的小协程,考虑使用 std::suspend_always 以避免不必要的栈帧。
异常处理 co_await 前后使用 try/catch 捕获异常,避免全局崩溃。
内存池 对于大规模协程,可使用自定义 promise_type 的内存池,以减少分配次数。
任务拆分 通过 co_yield 产生子任务,让协程保持轻量,避免单协程阻塞。

6. 小结

C++ 协程通过将函数分割成可挂起的状态机,为异步编程提供了一种更接近同步语义的写法。其实现基于 promise_type 与 coroutine_handle,协程的栈不依赖调用栈,能够跨线程挂起与恢复。掌握 co_await 的 awaiter 机制、异常传播与内存模型,能让你在性能与易用性之间取得最佳平衡。希望本文能帮助你快速上手 C++ 协程,并在实际项目中充分发挥其优势。

发表评论