C++20 协程(Coroutines)入门指南

在 C++20 中,协程(coroutines)被正式加入标准库,为异步编程提供了极大的便利。相比传统的回调或线程模型,协程既能保持代码的同步写法,又能有效管理异步任务的执行。本文从协程的基本概念、关键字使用、编写协程函数、以及常见使用场景三个部分,带你快速上手 C++20 协程。

1. 协程基础概念

协程是可以在执行过程中“挂起”并在后续恢复执行的函数。它与普通函数的区别主要体现在:

  • 挂起点co_await, co_yield, co_return)决定了协程的暂停与恢复。
  • 协程返回类型 不是普通类型,而是一个 协程句柄std::coroutine_handle)或一个封装的状态对象。
  • 协程的生命周期由 协程状态对象 管理,编译器在内部生成相应的状态机。

2. 关键字与语法

关键字 作用 说明
co_await 挂起协程,等待 awaitable 对象完成 async/await 类似,支持自定义 awaitable 类型
co_yield 暂停协程并返回一个值给调用方 适用于生成器(generator)模式
co_return 结束协程并返回结果 与普通 return 不同,需配合 awaitablegenerator

2.1 awaitable 对象

一个对象如果实现了以下成员函数,就能被 co_await 直接使用:

bool await_ready();   // 是否已经就绪,若为 true 则不挂起
void await_suspend(std::coroutine_handle<> h); // 挂起时调用
auto await_resume();  // 恢复后返回值

标准库提供了 std::suspend_alwaysstd::suspend_never 等常用实现。

2.2 协程返回类型

典型的协程返回类型有三种:

  1. `std::future `:与线程库协作的异步结果。
  2. `generator `(如 `std::experimental::generator`):生成器模式。
  3. 自定义结构:如 `Task `,内部维护状态机并提供 `await_resume()`。

3. 编写一个简单的协程函数

下面演示一个异步等待两秒后返回字符串的协程。

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

struct SleepAwaitable {
    std::chrono::milliseconds duration;
    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<> h) const {
        std::thread([h, dur = duration]() {
            std::this_thread::sleep_for(dur);
            h.resume();
        }).detach();
    }
    void await_resume() const noexcept {}
};

std::future<std::string> async_task() {
    std::cout << "Task started\n";
    co_await SleepAwaitable{std::chrono::milliseconds(2000)};
    std::cout << "Task resumed\n";
    co_return "Hello, Coroutine!";
}

int main() {
    auto fut = async_task();
    std::cout << "Doing other work...\n";
    std::cout << fut.get() << '\n';
}

运行结果

Task started
Doing other work...
Task resumed
Hello, Coroutine!

3.1 关键点

  • 挂起co_await SleepAwaitable{...} 触发协程暂停,await_suspend 内部启动一个线程来延迟恢复。
  • 恢复:线程结束后调用 h.resume(),协程继续执行 co_return
  • 返回co_return 把结果封装进 std::future,主线程通过 fut.get() 获取。

4. 生成器(Generator)示例

生成器是一种最常见的 co_yield 用法,用于一次性生成一系列值。C++20 标准中没有正式的 generator,但实验性库 std::experimental::generator 已经可以使用。

#include <experimental/generator>
#include <iostream>

std::experimental::generator <int> range(int start, int end) {
    for (int i = start; i < end; ++i)
        co_yield i;
}

int main() {
    for (int v : range(1, 5))
        std::cout << v << ' ';
    // 输出:1 2 3 4
}

co_yield 在循环中产生值,调用方通过范围 for 自动获取下一个值。协程内部维护迭代器状态,暂停与恢复由编译器完成。

5. 常见使用场景

场景 说明
异步 I/O 如网络请求、文件读写,可用协程等待 I/O 完成,代码保持同步结构。
状态机 把复杂的状态机逻辑拆分为多个挂起点,提升可读性。
生成器 逐步生成序列、迭代器、流式数据处理。
协程池 通过协程句柄实现任务调度,降低线程上下文切换成本。

6. 性能与注意事项

  1. 编译器实现差异:不同编译器对协程支持程度不同,调试工具支持也有限。
  2. 堆栈分配:协程状态机默认在堆上分配,若协程大量创建需考虑内存消耗。
  3. 异常传播:协程内部抛出的异常会通过 await_resume 传递给调用方,需使用 try/catch 处理。

7. 结语

C++20 协程为异步编程提供了天然的同步语法,极大提升了代码的可读性和可维护性。虽然初始学习曲线略高,但只要掌握挂起点、awaitable 对象以及协程返回类型,便能在实际项目中快速落地。希望本文能帮助你打开协程的大门,进一步探索 C++ 的现代化特性。祝编码愉快!

发表评论