C++20 协程入门:从原理到手写第一个 Generator

C++20 协程入门:从原理到手写第一个 Generator

C++20 带来的协程(Coroutines)是近年来 C++ 最大的变革之一。但很多人兴冲冲地去学,却被复杂的概念劝退:promise_typeco_awaithandle……

与其他语言(如 Python、Go、C#)不同,C++20 的协程是一个“为库作者设计”的底层框架,而不是给最终用户直接使用的上层库。 这意味着标准库里甚至没有一个现成的 TaskGenerator 类,你必须自己造轮子。

本文将带你通过手写一个简单的 生成器(Generator),彻底搞懂 C++20 协程的运作机制。

1. 什么是协程?

用最通俗的话说:协程是一个可以“暂停”和“恢复”的函数。

  • 普通函数:一旦调用,就必须一口气执行完,中间不能停,最后返回结果并销毁栈帧。
  • 协程:执行过程中可以主动挂起(Suspend),保存当前的局部变量和状态,切出去干别的事;等时机成熟了,再从暂停的地方恢复(Resume)继续执行。

核心关键字

编译器只要在函数体里看到以下三个关键字之一,就会把这个函数当做协程处理:

  1. co_await:挂起协程,等待某个操作完成。
  2. co_yield:挂起协程,并像水管一样“产出”一个值给调用者(类似 Python 的 yield)。
  3. co_return:协程执行结束,返回最终结果。

2. 协程的三大金刚

要让协程跑起来,我们需要定义三个东西配合工作:

  1. Promise 对象 (promise_type)
    • 大脑。它住在协程的帧(Frame)里,负责控制协程的启动、结束、异常处理以及传值。
  2. 协程句柄 (std::coroutine_handle)
    • 遥控器。它是一个轻量级的指针,持有者可以通过它来手动“恢复”协程运行或“销毁”协程。
  3. 返回值对象(Return Object)
    • 外壳。这是协程函数返回给调用者的东西。它内部通常包裹着 coroutine_handle,方便调用者控制协程。

3. 实战:手写一个整数生成器

我们要实现的目标是:写一个函数,每次调用产生一个数字,暂停,下次调用继续产生下一个数字。

由于 C++20 没有内置 std::generator(C++23 才加入),我们需要自己写一个 IntGenerator 类。

第一步:搭架子

协程函数的返回值必须包含一个名为 promise_type 的嵌套类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <iostream>
#include <coroutine>

// 我们的协程返回类型
struct IntGenerator {
struct promise_type; // 前向声明

// 别名:对应的句柄类型
using handle_type = std::coroutine_handle<promise_type>;

// 句柄:我们的遥控器
handle_type h;

// 构造函数
IntGenerator(handle_type h) : h(h) {}

// 析构函数:非常重要!协程结束必须手动销毁
~IntGenerator() {
if (h) h.destroy();
}

// 禁止拷贝,只能移动(RAII)
IntGenerator(const IntGenerator&) = delete;
IntGenerator& operator=(const IntGenerator&) = delete;
IntGenerator(IntGenerator&& other) noexcept : h(other.h) {
other.h = nullptr;
}

// --- 下面是 Promise 的定义 ---
struct promise_type {
int current_value; // 用于存放 yield 出来的值

// 1. 协程创建时调用,生成外壳对象返回给调用者
IntGenerator get_return_object() {
return IntGenerator{handle_type::from_promise(*this)};
}

// 2. 协程初始化时的行为
// std::suspend_always 表示:协程一创建就立刻挂起,不执行代码
// 如果想一创建就跑,用 std::suspend_never
std::suspend_always initial_suspend() { return {}; }

// 3. 协程结束时的行为
// std::suspend_always 表示:结束后不销毁状态,让外部决定何时销毁
std::suspend_always final_suspend() noexcept { return {}; }

// 4. 当代码遇到 co_yield val; 时调用
std::suspend_always yield_value(int value) {
current_value = value; // 保存值
return {}; // 保存完立刻挂起
}

// 5. 当代码遇到 co_return; 时调用
void return_void() {}

// 6. 异常处理
void unhandled_exception() {
std::exit(1);
}
};

// --- 外部使用的接口 ---

// 恢复协程执行,获取下一个值
bool next() {
if (!h || h.done()) return false;
h.resume(); // 按下“播放键”
return !h.done(); // 如果还没结束,说明产出了新值
}

// 获取当前值
int value() const {
return h.promise().current_value;
}
};

第二步:编写协程函数

有了上面的 IntGenerator 类,我们就可以像写普通代码一样写协程了。

1
2
3
4
5
6
7
// 这是一个协程,因为它用到了 co_yield
IntGenerator createCounter(int max) {
for (int i = 0; i < max; ++i) {
co_yield i; // 1. 传出 i; 2. 暂停执行; 3. 等待外部 resume
}
// 隐式 co_return;
}

第三步:在 main 中调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main() {
// 1. 创建协程
// 此时协程被创建,分配了堆内存,但卡在 initial_suspend,代码一行都没跑
auto gen = createCounter(3);

std::cout << "Start..." << std::endl;

// 2. 循环驱动协程
while (gen.next()) { // 调用 next -> h.resume() -> 协程跑到 co_yield -> 暂停
std::cout << "Got: " << gen.value() << std::endl;
}

std::cout << "End." << std::endl;
return 0;
}

运行结果

1
2
3
4
5
Start...
Got: 0
Got: 1
Got: 2
End.

4. 发生了什么?(执行流解析)

理解这一段非常关键,请配合代码看:

  1. Main: 调用 createCounter(3)
  2. Compiler:
    • 在堆上分配协程帧(存放 i, max, promise)。
    • 调用 promise.get_return_object(),创建 gen 对象返回给 Main。
    • 调用 promise.initial_suspend(),协程挂起
  3. Main: 拿到 gen,输出 “Start…”。
  4. Main: 进入 while,调用 gen.next() -> h.resume()
  5. Coroutine:
    • 从挂起点恢复,执行 for 循环初始化。
    • 遇到 co_yield 0
    • 调用 promise.yield_value(0),将 0 存入 promise.current_value
    • yield_value 返回 suspend_always,协程再次挂起
  6. Main: h.resume() 返回,next() 返回 true。
  7. Main: 调用 gen.value(),从 promise 里拿出 0 打印。
  8. Main: 再次调用 gen.next()… 重复上述过程。
  9. Coroutine: 循环结束,隐式调用 co_return
  10. Coroutine: 调用 promise.return_void(),然后 final_suspend(),最后一次挂起。
  11. Main: next() 检测到 h.done() 为 true,循环结束。
  12. Main: gen 对象析构,调用 h.destroy(),释放堆内存。

5. 避坑指南

对于初学者,C++ 协程有两个巨大的坑:

坑一:参数的生命周期

协程的参数是按值复制还是按引用传递需要非常小心。

1
2
3
4
5
6
// 危险!
IntGenerator badCoroutine(const std::string& str) {
co_yield 1; // 挂起
// 恢复时,如果外部的 str 已经被销毁了,这里访问 str 就会崩溃!
std::cout << str << std::endl;
}

建议:对于协程参数,尽量传值(By Value)或者确保引用的对象在协程结束前一直存活。

坑二:内存泄漏

协程帧通常是在上分配的。如果你的 Wrapper 类(如上面的 IntGenerator)没有在析构函数里调用 h.destroy(),那块内存就永远泄露了。这和普通函数的栈内存自动回收完全不同。


总结

C++20 协程虽然写法繁琐,但本质上就是编译器帮你把函数切成碎片,并生成了一个状态机

  • Promise 定义规则。
  • Handle 控制流程。
  • Yield/Await 切割代码块。

作为应用层开发者,如果你不想手写这些 boilerplate(样板代码),可以使用成熟的库,如 cppcoro,或者等待 C++23 的 std::generator。但理解了本文的 promise_type,你也就掌握了 C++ 异步编程的最底层钥匙。


C++20 协程入门:从原理到手写第一个 Generator
https://www.psnow.sbs/2025/12/29/C-20-协程全面解析:下一代异步编程模型/
作者
Psnow
发布于
2025年12月30日
许可协议