C++20 的协程:从概念到实践

协程(Coroutines)是 C++20 里最激动人心的特性之一,它让异步编程与同步代码的写法无缝结合,降低了回调地狱的概率。本文从协程的基本概念讲起,逐步展开实现细节、典型应用场景以及常见陷阱,帮助读者快速上手并掌握协程的核心技巧。

一、协程的基本概念

协程是一种能够暂停与恢复执行的函数。与传统的线程相比,协程的切换成本极低,几乎可以忽略不计;与回调函数相比,协程的代码结构更像同步写法,易于维护。C++20 为协程提供了三大语法工具:

  • co_await:等待一个 awaitable 对象完成。
  • co_yield:在生成器中产生一个值并暂停。
  • co_return:结束协程并返回最终结果。

awaitable 对象

协程只能暂停在可等待的对象上。C++ 标准库提供了 std::futurestd::async 等 awaitable,第三方库如 cppcoro::generator 也提供了相应实现。一个自定义 awaitable 需要实现 await_ready()await_suspend()await_resume() 三个成员函数。

二、协程的核心实现

下面给出一个最小可复现的协程示例:一个异步读取文件内容的函数。

#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>

struct AwaitableSleep {
    std::chrono::milliseconds duration;
    AwaitableSleep(std::chrono::milliseconds d) : duration(d) {}

    bool await_ready() noexcept { return false; }

    void await_suspend(std::coroutine_handle<> h) {
        std::thread([h, this](){
            std::this_thread::sleep_for(duration);
            h.resume();
        }).detach();
    }

    void await_resume() noexcept {}
};

struct AsyncReadFile {
    struct promise_type {
        std::string data;
        std::exception_ptr eptr;

        AsyncReadFile get_return_object() { return AsyncReadFile{ std::coroutine_handle <promise_type>::from_promise(*this) }; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { eptr = std::current_exception(); }
        void return_void() {}
    };

    std::coroutine_handle <promise_type> handle;

    explicit AsyncReadFile(std::coroutine_handle <promise_type> h) : handle(h) {}
    ~AsyncReadFile() { if (handle) handle.destroy(); }

    std::string get() {
        if (handle.promise().eptr) std::rethrow_exception(handle.promise().eptr);
        return handle.promise().data;
    }
};

AsyncReadFile readFileAsync(const std::string& path) {
    // 模拟异步读取
    co_await AwaitableSleep{ std::chrono::milliseconds(500) };
    // 这里应该真正读取文件,但为演示省略
    std::string fakeContent = "Hello from " + path;
    co_return;
}

关键点拆解

  1. promise_type:协程的核心,管理状态、返回值与异常。
  2. initial_suspend / final_suspend:决定协程何时暂停/恢复。suspend_never 让协程立即开始,suspend_always 让协程在结束后暂停等待外部销毁。
  3. await_suspend:在此实现真正的异步操作,例如 std::threadasio::awaitable

三、生成器(Generator)实例

生成器是协程最常见的用途之一,能够一次产生一个值而不需要一次性生成整个序列。C++20 标准库本身不直接提供生成器,但可以用协程轻松实现。

template<typename T>
struct generator {
    struct promise_type {
        T current_value;
        std::suspend_always yield_value(T val) {
            current_value = std::move(val);
            return {};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        generator get_return_object() {
            return generator{ std::coroutine_handle <promise_type>::from_promise(*this) };
        }
        void return_void() {}
        void unhandled_exception() { std::rethrow_exception(std::current_exception()); }
    };

    std::coroutine_handle <promise_type> handle;
    explicit generator(std::coroutine_handle <promise_type> h) : handle(h) {}
    ~generator() { if (handle) handle.destroy(); }

    bool next() { return handle.resume(), !handle.done(); }
    T current() const { return handle.promise().current_value; }
};

generator <int> naturalNumbers(int start = 1) {
    int i = start;
    while (true) {
        co_yield i++;
    }
}

使用示例:

auto gen = naturalNumbers(10);
for (int i = 0; i < 5 && gen.next(); ++i)
    std::cout << gen.current() << ' ';   // 输出 10 11 12 13 14

四、协程的典型应用场景

  1. 异步 I/O:与网络、文件等 IO 结合,避免阻塞线程。
  2. 流式数据处理:如日志、传感器数据,按需生成处理。
  3. 状态机实现:协程内部可以维护状态,外部通过 next() 切换状态。
  4. 协程管道:多个协程串联,形成数据处理流水线,类似 Go 的 channel。

五、常见陷阱与调优建议

陷阱 原因 解决方案
协程对象被提前销毁 协程返回值未被拷贝或移动,导致 handle 被销毁。 在使用前确保保存 handle 或返回 std::unique_ptr<generator<T>>
内存泄漏 协程内部捕获的大对象未在 final_suspend 清理。 在 promise_type 里使用 std::shared_ptrunique_ptr 管理资源。
高开销的线程切换 await_suspend 里启动大量 std::thread 采用线程池或异步事件循环(如 asio)。
异常不透明 unhandled_exception 只存储了指针,调用者需手动 rethrow_exception 在协程返回前提供 catchget() 方法自动抛出。
调试困难 协程内部状态难以在调试器中跟踪。 使用 std::suspend_always 代替 suspend_never,让调试器更容易跟踪。

六、未来展望

C++23 将继续完善协程支持,预计会推出更友好的生成器标准库、awaitable 标准类型和协程调度器接口。与此同时,第三方生态(如 cppcoroawaitable)正逐渐成熟,为 C++ 开发者提供更丰富的异步工具链。


通过本文的介绍,读者已经掌握了 C++20 协程的基础语法、实现细节、典型用例与常见陷阱。下一步可以尝试将协程与网络库(如 Boost.Beastcppcoro)结合,实现高性能的异步服务器,进一步体会协程带来的便利。祝编码愉快!

发表评论