一、C20协程:从函数到可暂停的计算单元
说实话,我第一次接触C20协程时,脑子里冒出的第一个念头是——这不就是函数版的"暂停键"吗?
我们平时写的函数,一旦调用,就得从头跑到尾。但协程不一样。它可以在执行中途停下来,把控制权交回去,然后过一会儿再回来接着跑。嗯,就像你写代码写到一半,去泡杯咖啡,回来继续写一样。
我个人习惯把协程理解成"有状态的函数"。普通函数每次调用都是全新的开始,局部变量重新分配。但协程能记住上次停在哪,局部变量还在不在?这就要说到协程帧了。
1.1 协程 vs 普通函数:核心区别
| 特性 | 普通函数 | 协程 |
|---|---|---|
| 执行方式 | 一次性执行到底 | 可暂停、可恢复 |
| 栈帧生命周期 | 调用时创建,返回时销毁 | 可能跨多次调用存活 |
| 局部变量 | 每次调用重新分配 | 在协程帧中持久保存 |
| 返回方式 | return | co_return / co_yield / co_await |
你看,协程本质上是一种"可挂起的函数"。它用三个新关键字来管理生命周期:co_await、co_yield、co_return。这三个家伙,就是协程的"暂停、产出、结束"三件套。
二、三个核心关键字:co_await / co_yield / co_return
2.1 co_await:等待一个可挂起的操作
co_await 是协程里最常用的关键字。它的作用是:挂起当前协程,等待某个操作完成,然后恢复执行。
我在项目中遇到过这样一个场景:需要从多个传感器异步读取数据。如果用传统回调,代码会碎成一地。用 co_await 就清爽多了——
// 伪代码:协程方式读取传感器
Task<SensorData> readSensor() {
// 发起异步读取,挂起等待
auto raw = co_await async_read(sensor_pin);
// 恢复后继续处理
SensorData result = process(raw);
co_return result;
}
这里 co_await async_read(...) 会触发挂起。等数据准备好了,协程自动恢复,接着往下走。你想想看,这不就是"同步写法,异步执行"吗?
2.2 co_yield:产出值,但不结束
co_yield 有点像生成器里的 yield。它把当前值"吐"出去,然后挂起自己,等下次被唤醒再继续。
说白了,co_yield 就是"我还没干完,先给你一个中间结果"。
Generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a; // 产出当前值,挂起
int next = a + b;
a = b;
b = next;
}
}
调用方每次取一个值,协程就推进一次。这种"惰性求值"的模式,在处理无限序列或流式数据时特别有用。
2.3 co_return:结束协程
co_return 就是协程的 return。它表示协程执行完毕,返回最终结果。
注意:一旦协程里出现了 co_return,就不能再用普通的 return 了。这是编译器强制的规则。
Task<int> compute() {
int sum = 0;
for (int i = 0; i < 10; ++i) {
sum += i;
co_await step(); // 每步都挂起一下
}
co_return sum; // 最终结果
}
三、协程帧与栈管理:协程的"记忆体"
协程能暂停和恢复,靠的是什么?答案是——协程帧(coroutine frame)。
普通函数的局部变量存在栈上,函数返回栈帧就销毁了。但协程不一样。它暂停时,局部变量不能丢,得找个地方存起来。这个"地方"就是堆上分配的协程帧。
3.1 协程帧里有什么?
- 局部变量:所有在协程体内定义的自动变量
- 挂起点信息:记录上次执行到哪一行,恢复时从哪继续
- promise 对象:协程的"管家",管理协程状态和返回值
- 参数拷贝:协程参数会拷贝到帧里(按值传递)
说白了,协程帧就是协程的"记忆体"。它让协程在挂起后,所有状态都完好无损。
3.2 栈管理:堆分配与优化
协程帧默认在堆上分配。这意味着每次创建协程,都可能有一次 malloc。高频创建协程时,这会是性能瓶颈。
我个人的建议是:
- 对性能敏感的场景,考虑使用自定义分配器(通过 promise_type 的 operator new)
- 避免在热路径上频繁创建和销毁协程
- 协程帧的大小在编译期就确定了,所以分配开销是可控的
// 自定义分配器示例
struct MyPromise {
// 自定义协程帧分配
void* operator new(size_t size) {
return my_pool_alloc(size);
}
void operator delete(void* ptr, size_t size) {
my_pool_free(ptr, size);
}
// ... 其他必要方法
};
四、知识体系总览
下面这张图,是我梳理的协程核心知识结构。你可以把它当作学习路线图——
五、避坑指南与个人经验
讲到这里,我想分享几个实际项目中遇到的教训——
- 协程帧的生命周期管理:我曾经在协程里捕获了一个局部变量的引用,结果协程挂起后,那个变量所在的栈帧已经被销毁了。恢复时访问悬空引用,程序直接崩溃。记住:协程帧里保存的是参数的拷贝,不是引用。
- 不要滥用 co_await:有些开发者喜欢在每个函数前都加 co_await,觉得这样"更异步"。其实没必要。只有真正需要挂起的操作才用 co_await。否则只会增加协程帧分配的开销。
- 协程不是线程:协程是"可挂起的函数",不是并发执行单元。它可以在单线程里实现协作式多任务,但不会利用多核。如果你需要并行,还是得用线程。
好了,关于 C20 协程的基本概念,我们就聊到这里。co_await 让你能等待异步操作,co_yield 让你能产出中间值,co_return 用来结束协程。而协程帧,就是这一切能实现的"幕后功臣"。
下次写异步代码时,不妨试试协程。你会发现,原来"暂停"和"恢复"可以这么优雅。
公众号:蓝海资料掘金营,微信 deep3321