## C++20 协程的实战指南

C++20 引入了协程(coroutines),为异步编程提供了一套简洁而强大的语法。协程允许函数在执行过程中挂起并恢复,极大地提升了代码的可读性与性能。本文将从基本概念、协程的实现机制、常见用例到实战案例,系统阐述如何在实际项目中使用 C++20 协程。


1. 协程概念回顾

  • 挂起(suspend): 协程可以在任意位置暂停执行,并将当前状态保存到协程框架。
  • 恢复(resume): 在需要时重新激活协程,从挂起点继续执行。
  • 协程返回值: 与普通函数不同,协程返回的是一个 协程对象(如 `generator ` 或 `task`),而非具体的值。

C++20 对协程的支持依赖于以下关键关键字:

关键字 作用
co_await 暂停当前协程,等待一个可等待对象完成
co_yield 产生一个值,并挂起协程,等到下次 resume 时继续
co_return 结束协程并返回最终值(如果有)

2. 协程的实现机制

2.1 协程句柄(std::coroutine_handle

协程的底层由 std::coroutine_handle 管理。每个协程都有一个隐式生成的帧(frame),其中保存了协程状态、局部变量和调用栈信息。coroutine_handle 可以:

  • 检查是否完成 (done() )
  • 恢复协程 (resume())
  • 释放资源 (destroy())

2.2 协程 promise

协程的返回类型需要提供一个 promise_type,用来定义协程的生命周期事件:

struct generator_promise {
    using value_type = int; // 示例

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

2.3 关键生命周期方法

  • initial_suspend():协程开始时是否立即挂起。
  • final_suspend():协程结束后是否挂起,允许执行清理代码。
  • yield_value():处理 co_yield,保存产出的值。

3. 常见协程类型

类型 典型用法 关键特性
`generator
| 生成序列 | 通过co_yield` 产生值
`task
| 异步任务 | 通过co_await` 等待
`async_generator
` 异步生成器 结合异步 IO 使用

C++20 标准库中未提供完整实现,需要自己编写或使用第三方库(如 cppcoro、Microsoft Concurrency Runtime)。


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

下面给出一个完整示例:使用协程读取文件内容,每行返回一次,演示协程的 co_await 与异步 IO 结合。

4.1 依赖

  • C++20 编译器(GCC 10+ 或 Clang 10+)
  • asio(Boost.Asio 或 standalone Asio)支持异步文件 I/O

4.2 代码实现

#include <asio.hpp>
#include <iostream>
#include <string>
#include <vector>
#include <coroutine>
#include <optional>
#include <future>

using asio::awaitable;
using asio::co_spawn;
using asio::detached;
using asio::ip::tcp;
using namespace std::chrono_literals;

// 1. 读取一行文件的协程
awaitable<std::optional<std::string>> read_line(asio::random_access_handle& handle) {
    const std::size_t buf_size = 1024;
    std::vector <char> buffer(buf_size);
    std::size_t pos = 0;
    for (;;) {
        std::size_t n = co_await handle.async_read_some(asio::buffer(buffer.data() + pos, buf_size - pos),
                                                       asio::use_awaitable);
        if (n == 0) {
            // EOF
            if (pos == 0) co_return std::nullopt;
            buffer.resize(pos);
            break;
        }
        pos += n;
        // 检测是否出现换行
        if (std::find(buffer.begin(), buffer.begin() + pos, '\n') != buffer.begin() + pos) {
            break;
        }
        if (pos == buf_size) {
            // buffer full, but no newline yet
            buffer.resize(buf_size * 2);
        }
    }
    std::string line(buffer.data(), pos);
    co_return line;
}

// 2. 主协程,读取文件所有行
awaitable <void> read_file(const std::string& path) {
    asio::io_context io_context;
    asio::random_access_handle handle(io_context);
    co_await handle.open(path, std::ios::in);
    while (auto line_opt = co_await read_line(handle)) {
        std::cout << *line_opt << std::endl;
    }
    co_await handle.close();
}

// 3. 主入口
int main() {
    asio::io_context io_context;
    co_spawn(io_context, read_file("sample.txt"), detached);
    io_context.run();
    return 0;
}

说明

  • asio::random_access_handle 为异步文件句柄。
  • awaitable 为协程返回类型,表示异步操作。
  • co_await 在 I/O 完成前挂起协程。
  • co_spawn 用来启动协程任务。

5. 性能与调试

关注点 建议
堆栈占用 协程帧保存在堆上,避免深递归导致栈溢出
异常处理 promise_typeunhandled_exception 必须妥善处理
调试难度 采用 -g 调试,使用 gdb/lldbthread apply all bt 查看协程堆栈

6. 与现有项目的集成

  1. 逐步迁移:先在新模块使用协程,逐步把同步函数改写为协程版。
  2. 封装统一:为常用异步 I/O(网络、文件、数据库)创建统一的协程包装。
  3. 错误码:结合 std::expected 或自定义错误类型,在协程中统一处理错误。

7. 常见陷阱

  • 忘记 co_await:直接返回一个 awaitable 对象会导致挂起点不正确。
  • promise_type 与返回类型不匹配get_return_object() 必须返回与协程返回类型匹配的对象。
  • 资源泄露std::coroutine_handle 需要手动 destroy(),否则会泄露。

8. 进一步阅读

  • 《C++20 协程详解》 — 详尽剖析协程实现。
  • 《Boost.Asio 与 C++20 协程》 — 结合实际网络编程案例。
  • 《cppcoro》GitHub 项目 — 提供 generator, task 等实用实现。

结语

C++20 协程为语言提供了强大的异步编程模型。通过掌握其基本语法、生命周期与常见实现,可以让代码更简洁、并发更高效。希望本文的示例与技巧能帮助你在项目中顺利使用协程,开启 C++ 异步编程的新篇章。

发表评论