## 题目:C++20 协程(Coroutine)——让异步代码变得同步般自然

在 C++20 中,协程(Coroutine)被正式加入标准库,为开发者提供了一种更加优雅地编写异步、延迟计算和生成器等功能的手段。相比传统的回调、Future/Promise 以及第三方库,协程的语法更接近同步代码,降低了错误率并提升了可读性。本文将从协程的基本概念、实现原理到实际使用场景进行全面阐述,并给出完整示例,帮助你快速上手。


1. 协程到底是什么?

协程是一种能暂停和恢复执行的函数。它在执行过程中可以“挂起”(suspend)并返回控制权,待需要继续时再恢复(resume)。在 C++20 中,协程的实现核心是:

  • 协程函数:使用 co_awaitco_yieldco_return 关键字的函数。
  • 协程句柄std::coroutine_handle 对协程进行控制(挂起、恢复、销毁)。
  • 协程 Promisepromise_type 结构,负责协程的状态、返回值等。

2. 基本语法示例

下面演示一个最简洁的协程:生成整数序列。

#include <coroutine>
#include <iostream>

struct IntGenerator {
    struct promise_type {
        int current_value;
        std::suspend_always yield_value(int value) {
            current_value = value;
            return {};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        IntGenerator get_return_object() { return { std::coroutine_handle <promise_type>::from_promise(*this) }; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle <promise_type> handle;

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

    struct Iterator {
        std::coroutine_handle <promise_type> h;
        bool done = false;

        int operator*() const { return h.promise().current_value; }

        Iterator& operator++() {
            h.resume();
            done = h.done();
            return *this;
        }
    };

    Iterator begin() { handle.resume(); return {handle, handle.done()}; }
    Iterator end()   { return {handle, true}; }
};

IntGenerator count(int n) {
    for (int i = 0; i < n; ++i)
        co_yield i;   // 暂停并返回当前值
}

使用方式:

int main() {
    for (int x : count(5))
        std::cout << x << ' ';   // 输出 0 1 2 3 4
}

解释

  • co_yield 将当前值保存到 promise,并暂停协程。
  • Iterator 调用 handle.resume() 继续执行,直到遇到下一个 co_yield 或协程结束。
  • co_awaitco_returnco_yield 类似,只是语义不同。

3. 协程的工作机制

  1. 编译阶段
    编译器将协程函数拆分为若干基本块,并生成一个 promise_type 结构。协程函数的返回类型必须满足 has_return_object,即提供 get_return_object()

  2. 运行时

    • 创建协程句柄 handle 并绑定到 promise_type
    • initial_suspend() 决定是否立即挂起;若返回 std::suspend_always,协程立即挂起。
    • co_yield/co_await/co_return 在执行时调用对应的 promise 方法,并根据返回值决定是否挂起。
    • final_suspend() 决定协程结束时是否再次挂起(常见返回 std::suspend_always 以防资源泄漏)。
  3. 资源管理
    std::coroutine_handle 提供 destroy()resume() 等操作,协程结束后需显式销毁。


4. 常见协程模式

4.1 生成器(Generator)

如前例所示,co_yield 用于生成序列。适用于大数据流、懒加载等场景。

4.2 异步 I/O(Async I/O)

结合 co_await 与事件循环框架(如 asio 或自定义 io_context)实现非阻塞 I/O。示例(伪代码):

struct AsyncRead {
    struct promise_type {
        // ...
        std::suspend_always await_suspend(std::coroutine_handle<> h) {
            // 注册 I/O 完成回调,完成时 resume 协程
            register_read_cb([h]{ h.resume(); });
            return {};
        }
        int await_resume() { return /* 读取的数据 */; }
    };
};

AsyncRead read_file(int fd, char* buffer, std::size_t size) {
    co_return co_await async_read(fd, buffer, size);
}

4.3 流程控制(State Machines)

使用 co_await 与自定义 Awaitable 对象可实现简洁的状态机,避免深层回调。

4.4 并行/并发(Parallel)

通过 co_spawn(如 boost::asio::spawn)或自定义调度器,将协程作为轻量级任务提交到线程池,利用协程的协作式调度提升并发度。


5. 与传统 Future/Promise 的对比

特点 协程(Coroutine) Future/Promise
语法 co_awaitco_yield future.get()
错误传播 通过异常传播 需要显式捕获
资源管理 自动销毁 需要手动管理
可读性 类似同步代码 回调链式较难
性能 轻量级协作式调度 线程/任务开销大

6. 开发工具与编译器支持

  • GCC:自 10.2 起已完整实现 C++20 协程。
  • Clang:自 10 起支持协程,推荐使用 -std=c++20
  • MSVC:从 Visual Studio 2019 版本 16.11 开始支持协程。
  • cppcorocppcoro::generatorcppcoro::task 等第三方实现。
  • IDE:CLion、VSCode(C++插件)均已支持协程语法高亮与调试。

7. 示例:异步文件读取 + 生成器

#include <coroutine>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <filesystem>

using namespace std::chrono_literals;

// 简易异步 I/O 伪实现
struct AwaitableRead {
    std::string file;
    std::string& buffer;
    std::size_t offset;
    std::size_t size;

    AwaitableRead(std::string f, std::string& buf, std::size_t o, std::size_t s)
        : file(std::move(f)), buffer(buf), offset(o), size(s) {}

    bool await_ready() const noexcept { return false; }

    void await_suspend(std::coroutine_handle<> h) const {
        // 异步读取(此处用同步模拟)
        std::ifstream ifs(file, std::ios::binary);
        ifs.seekg(offset);
        buffer.resize(size);
        ifs.read(&buffer[0], size);
        h.resume();
    }

    std::string await_resume() const { return buffer; }
};

struct AsyncReadFile {
    std::string file;
    std::coroutine_handle<> handle;

    AsyncReadFile(std::string f) : file(std::move(f)) {}
    ~AsyncReadFile() { if (handle) handle.destroy(); }

    struct promise_type {
        std::string result;
        std::coroutine_handle <promise_type> get_return_object() {
            return std::coroutine_handle <promise_type>::from_promise(*this);
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { std::terminate(); }
        void return_value(std::string r) { result = std::move(r); }
    };

    struct Iterator {
        std::coroutine_handle <promise_type> h;
        std::size_t chunk = 0;
        static constexpr std::size_t CHUNK_SIZE = 1024;

        Iterator(std::coroutine_handle <promise_type> h_) : h(h_) {}

        std::string operator*() {
            std::string buffer;
            h.promise().return_value(co_await AwaitableRead(h.promise().result, buffer, chunk, CHUNK_SIZE));
            return buffer;
        }

        Iterator& operator++() { ++chunk; return *this; }
        bool operator!=(const Iterator& other) const { return h != other.h; }
    };

    Iterator begin() { handle.resume(); return {handle}; }
    Iterator end() { return {handle}; }
};

int main() {
    // 假设存在大文件 "large.bin"
    for (auto chunk : AsyncReadFile("large.bin")) {
        std::cout << "读取到 " << chunk.size() << " 字节\n";
    }
}

提示:真实项目中请使用成熟的异步 I/O 库(如 asiolibuv)来实现 AwaitableRead,此处仅为演示。


8. 常见坑与调试技巧

  1. 忘记 return:协程函数若未 co_return,编译器会报 unreachable 错误。
  2. 资源泄漏:若协程抛异常未被捕获,promise_type 可能未正确销毁;使用 try/catchfinal_suspend() 处理。
  3. 调试难度:协程内部状态隐藏在生成的状态机里,可通过 -fdump-tree-original 等编译器选项查看展开后的代码。
  4. 调度器选择:默认协程在当前线程恢复,若需多线程请自定义 std::coroutine_traits 或使用第三方调度器。

9. 结语

C++20 协程为语言本身注入了强大的异步能力,使得传统的回调地狱、链式 Promise 以及繁琐的线程同步得以简化。只需几行代码即可实现高性能、易读的生成器、异步 I/O 与并发任务。掌握协程将为你在现代 C++ 开发中打开新的可能性——从嵌入式系统到高性能服务器,协程都能发挥其独特优势。希望本文能帮助你快速上手,并在项目中实际运用协程,提升代码质量与开发效率。祝编码愉快!

发表评论