C++20 协程的使用与实践

C++20 标准引入了协程(coroutine)这一强大的语言特性,旨在简化异步编程、协程以及生成器等场景。相比传统的回调或基于线程的并发模型,协程通过编译器生成的状态机实现了轻量级、可组合的执行单元。本文将从概念、实现原理、编程实践以及常见陷阱四个方面,系统剖析 C++20 协程,并给出可直接应用的代码示例。

1. 协程基础概念

  • 协程函数:使用 co_awaitco_yieldco_return 的函数。编译器会把它展开为一个状态机,返回一个可调用对象(promise type)。
  • 协程句柄:`std::coroutine_handle

    `,用于控制协程的生命周期(挂起、恢复、销毁)。

  • 协程状态:包括 suspendedrunningcompleted 等。

1.1 co_awaitco_yield

  • co_await:等待一个 awaitable 对象。等待期间协程挂起,调用方可以继续执行。
  • co_yield:产生一个值,并挂起协程。被 co_await 的地方可一次性收集所有产生的值。

1.2 Awaitable 对象

任何类型只要实现 await_ready()await_suspend()await_resume() 三个成员函数(或全局函数重载)即可被 co_await

2. 协程实现原理

编译器将协程函数展开为一个类(promise type)和一个状态机函数。简化过程如下:

  1. 调用协程函数时,返回一个 `std::coroutine_handle

    `,并构造 promise 对象。

  2. co_awaitco_yield 被翻译成调用对应的 awaitable 方法。
  3. co_return 把返回值存储到 promise 对象中。
  4. 当协程结束时,destroy 方法被调用。

这一过程保证了协程的“轻量级”,因为只有一次堆分配(如果需要)和一个栈帧即可。

3. 实战案例:异步 I/O 与生成器

3.1 异步读取文件

下面给出一个基于 asio 的异步读取文件示例,利用协程隐藏事件循环的细节。

#include <boost/asio.hpp>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <experimental/coroutine>

namespace asio = boost::asio;
using asio::ip::tcp;
using std::string;

// Awaitable 读取文件的异步操作
class AsyncReadFile {
public:
    AsyncReadFile(const string& path, std::size_t chunkSize)
        : file_(path, std::ios::binary), chunkSize_(chunkSize) {}

    struct promise_type {
        AsyncReadFile* self;
        std::vector <char> buffer;

        auto get_return_object() {
            return AsyncReadFile(self);
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        void return_void() {}
        void unhandled_exception() {
            std::terminate();
        }
    };

    using handle_type = std::experimental::coroutine_handle <promise_type>;

    AsyncReadFile(AsyncReadFile* self) : self_(self) {}

    handle_type coroHandle() { return handle_type::from_promise(*self_); }

    std::experimental::generator<std::vector<char>> operator()() {
        while (file_) {
            std::vector <char> chunk(chunkSize_);
            file_.read(chunk.data(), chunkSize_);
            std::size_t n = file_.gcount();
            if (n > 0) {
                chunk.resize(n);
                co_yield chunk;
            }
        }
    }

private:
    std::ifstream file_;
    std::size_t chunkSize_;
    // promise_type self_;
};

说明:这里演示了如何把文件读取过程包装成一个生成器,利用 co_yield 逐块返回数据。可以进一步改造为真正的 awaitable,使用 asio::awaitable

3.2 简易生成器

下面实现一个通用生成器,生成从 0 开始递增的整数。

#include <experimental/coroutine>
#include <iostream>

template<typename T>
class Generator {
public:
    struct promise_type {
        T value_;
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(T v) {
            value_ = v;
            return {};
        }
        Generator get_return_object() {
            return Generator{std::experimental::coroutine_handle <promise_type>::from_promise(*this)};
        }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    using handle_type = std::experimental::coroutine_handle <promise_type>;

    explicit Generator(handle_type h) : handle_(h) {}
    ~Generator() { if (handle_) handle_.destroy(); }

    bool next() {
        if (!handle_.done()) handle_.resume();
        return !handle_.done();
    }

    T current() const { return handle_.promise().value_; }

private:
    handle_type handle_;
};

Generator <int> count_to(int max) {
    for (int i = 0; i <= max; ++i)
        co_yield i;
}

使用方式:

int main() {
    auto gen = count_to(10);
    while (gen.next())
        std::cout << gen.current() << ' ';
    // 输出:0 1 2 3 4 5 6 7 8 9 10
}

4. 常见陷阱与最佳实践

  1. 忘记销毁协程句柄
    协程对象持有句柄,如果不手动销毁或返回时保证句柄可析构,可能导致内存泄漏。建议使用 std::coroutine_handle 的 RAII 包装器,或者返回 std::future/asio::awaitable

  2. 在协程内部使用阻塞操作
    co_await 的核心是非阻塞等待;如果在协程里调用了阻塞函数,线程将被挂起,失去协程的优势。一定要使用异步 API。

  3. 过度使用协程
    对于简单的同步代码,引入协程会增加编译时间和调试难度。仅在真正需要异步或生成器特性时使用。

  4. 异常传播
    协程内部抛出的异常会被包装到 promise 的 unhandled_exception()。如果你需要在调用方捕获,需在外层再次 co_await 并捕获异常。

  5. 跨平台标准库实现差异
    标准库对 std::coroutine_handle 的支持在不同编译器/标准库版本间略有差异。建议在使用前检查编译器的协程支持状态(如 -std=c++20 -fcoroutines)。

5. 结语

C++20 协程为 C++ 带来了强大的异步编程模型,既可以让代码像同步那样直观,也能在底层实现高性能的事件驱动。通过深入理解协程的语义、状态机实现与 awaitable 机制,程序员可以构建更清晰、更易维护的异步代码。未来随着标准化进一步完善,协程将成为 C++ 生态中不可或缺的工具。祝你在协程的世界里玩得开心、写出高效、简洁的代码!

发表评论