C++20 协程入门:从原理到手写第一个 Generator
C++20 协程入门:从原理到手写第一个 Generator
C++20 带来的协程(Coroutines)是近年来 C++ 最大的变革之一。但很多人兴冲冲地去学,却被复杂的概念劝退:promise_type、co_await、handle……
与其他语言(如 Python、Go、C#)不同,C++20 的协程是一个“为库作者设计”的底层框架,而不是给最终用户直接使用的上层库。 这意味着标准库里甚至没有一个现成的 Task 或 Generator 类,你必须自己造轮子。
本文将带你通过手写一个简单的 生成器(Generator),彻底搞懂 C++20 协程的运作机制。
1. 什么是协程?
用最通俗的话说:协程是一个可以“暂停”和“恢复”的函数。
- 普通函数:一旦调用,就必须一口气执行完,中间不能停,最后返回结果并销毁栈帧。
- 协程:执行过程中可以主动挂起(Suspend),保存当前的局部变量和状态,切出去干别的事;等时机成熟了,再从暂停的地方恢复(Resume)继续执行。
核心关键字
编译器只要在函数体里看到以下三个关键字之一,就会把这个函数当做协程处理:
co_await:挂起协程,等待某个操作完成。co_yield:挂起协程,并像水管一样“产出”一个值给调用者(类似 Python 的 yield)。co_return:协程执行结束,返回最终结果。
2. 协程的三大金刚
要让协程跑起来,我们需要定义三个东西配合工作:
- Promise 对象 (
promise_type):- 大脑。它住在协程的帧(Frame)里,负责控制协程的启动、结束、异常处理以及传值。
- 协程句柄 (
std::coroutine_handle):- 遥控器。它是一个轻量级的指针,持有者可以通过它来手动“恢复”协程运行或“销毁”协程。
- 返回值对象(Return Object):
- 外壳。这是协程函数返回给调用者的东西。它内部通常包裹着
coroutine_handle,方便调用者控制协程。
- 外壳。这是协程函数返回给调用者的东西。它内部通常包裹着
3. 实战:手写一个整数生成器
我们要实现的目标是:写一个函数,每次调用产生一个数字,暂停,下次调用继续产生下一个数字。
由于 C++20 没有内置 std::generator(C++23 才加入),我们需要自己写一个 IntGenerator 类。
第一步:搭架子
协程函数的返回值必须包含一个名为 promise_type 的嵌套类型。
1 | |
第二步:编写协程函数
有了上面的 IntGenerator 类,我们就可以像写普通代码一样写协程了。
1 | |
第三步:在 main 中调用
1 | |
运行结果
1 | |
4. 发生了什么?(执行流解析)
理解这一段非常关键,请配合代码看:
- Main: 调用
createCounter(3)。 - Compiler:
- 在堆上分配协程帧(存放
i,max,promise)。 - 调用
promise.get_return_object(),创建gen对象返回给 Main。 - 调用
promise.initial_suspend(),协程挂起。
- 在堆上分配协程帧(存放
- Main: 拿到
gen,输出 “Start…”。 - Main: 进入
while,调用gen.next()->h.resume()。 - Coroutine:
- 从挂起点恢复,执行
for循环初始化。 - 遇到
co_yield 0。 - 调用
promise.yield_value(0),将 0 存入promise.current_value。 yield_value返回suspend_always,协程再次挂起。
- 从挂起点恢复,执行
- Main:
h.resume()返回,next()返回 true。 - Main: 调用
gen.value(),从 promise 里拿出 0 打印。 - Main: 再次调用
gen.next()… 重复上述过程。 - Coroutine: 循环结束,隐式调用
co_return。 - Coroutine: 调用
promise.return_void(),然后final_suspend(),最后一次挂起。 - Main:
next()检测到h.done()为 true,循环结束。 - Main:
gen对象析构,调用h.destroy(),释放堆内存。
5. 避坑指南
对于初学者,C++ 协程有两个巨大的坑:
坑一:参数的生命周期
协程的参数是按值复制还是按引用传递需要非常小心。
1 | |
建议:对于协程参数,尽量传值(By Value)或者确保引用的对象在协程结束前一直存活。
坑二:内存泄漏
协程帧通常是在堆上分配的。如果你的 Wrapper 类(如上面的 IntGenerator)没有在析构函数里调用 h.destroy(),那块内存就永远泄露了。这和普通函数的栈内存自动回收完全不同。
总结
C++20 协程虽然写法繁琐,但本质上就是编译器帮你把函数切成碎片,并生成了一个状态机。
- Promise 定义规则。
- Handle 控制流程。
- Yield/Await 切割代码块。
作为应用层开发者,如果你不想手写这些 boilerplate(样板代码),可以使用成熟的库,如 cppcoro,或者等待 C++23 的 std::generator。但理解了本文的 promise_type,你也就掌握了 C++ 异步编程的最底层钥匙。