一、协程是什么?我当年也困惑过
说实话,我第一次接触 C++20 协程时,也是一头雾水。那时候我还在用 C++11 的 std::async 和线程池,觉得异步编程已经够用了。直到有一次,我在写一个网络库,回调嵌套了五六层,代码简直没法看。嗯,那时候我才意识到,协程这东西,真不是花架子。
协程,说白了就是可以暂停和恢复的函数。普通函数一旦调用,就得从头跑到尾。协程不一样,它可以在中间停下来,等条件满足了再接着跑。你想想看,这像不像一个可以随时挂起的任务?
二、三个关键字:co_await、co_yield、co_return
这三个关键字,是协程的「三驾马车」。我习惯把它们理解为三种不同的「暂停方式」。
2.1 co_await:等一个结果
co_await 是最常用的。它表示「我在这里等一个东西,等到了再继续」。比如网络请求、文件读取、定时器等待。
// 一个简单的协程示例
#include <coroutine>
#include <iostream>
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
Task myCoroutine() {
std::cout << "第一步:开始执行\n";
co_await std::suspend_always{}; // 暂停在这里
std::cout << "第二步:恢复执行\n";
}
int main() {
myCoroutine();
return 0;
}
我在项目中遇到过一个问题:co_await 后面跟的东西,必须是一个 Awaitable 对象。说白了,就是它得知道「什么时候该恢复你」。标准库提供了 std::suspend_always 和 std::suspend_never,前者总是暂停,后者从不暂停。
2.2 co_yield:吐出一个值
co_yield 有点像生成器。它把当前的值「吐」出来,然后暂停。调用者拿到值后,可以决定要不要继续。
// 一个生成斐波那契数列的协程
Generator fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a; // 吐出当前值,暂停
int next = a + b;
a = b;
b = next;
}
}
// 使用
auto gen = fibonacci();
for (int i = 0; i < 10; ++i) {
std::cout << gen.next() << " "; // 每次调用 next() 恢复协程
}
我个人习惯用 co_yield 来处理「流式数据」。比如逐行读取大文件、实时数据流处理。它比回调更直观,比线程更轻量。
2.3 co_return:结束协程
co_return 就是协程的 return。它表示协程执行完毕,可以返回一个值,也可以不返回。
Task compute() {
int result = 42;
co_return result; // 返回结果并结束
}
注意:co_return 和 return 不能混用。一旦函数体内出现了 co_await、co_yield 或 co_return 中的任何一个,这个函数就变成了协程,不能再使用普通的 return。
return,编译器报了一堆看不懂的错误。后来才明白,协程函数必须用 co_return,否则编译器会认为你写错了。
三、协程 vs 线程:到底谁更香?
很多新手会问:协程和线程有什么区别?我直接说结论:协程是用户态的,线程是内核态的。
| 对比维度 | 协程 | 线程 |
|---|---|---|
| 创建开销 | 极低(几十纳秒) | 较高(微秒级) |
| 切换开销 | 函数调用级别 | 内核态切换 |
| 并发模型 | 协作式(主动让出) | 抢占式(系统调度) |
| 内存占用 | 栈可定制(几KB) | 默认栈(1MB+) |
| 适用场景 | I/O密集型、高并发 | CPU密集型、多核并行 |
你想想看,如果你要处理一万个网络连接,开一万个线程?那内存直接爆炸。但用协程,每个协程只占几 KB 的栈空间,轻松搞定。
不过,协程也有短板。它不能利用多核 CPU 并行计算。因为协程本质上是单线程的,只是通过「暂停/恢复」来模拟并发。如果你要做 CPU 密集型的计算,还是老老实实用线程池吧。
四、实际应用场景:我踩过的坑和收获
4.1 网络编程:告别回调地狱
我记得以前写异步网络库,回调嵌套得跟千层饼似的。有了协程,代码可以写成同步风格,但底层是异步的。
// 伪代码:协程版 HTTP 请求
Task fetchHttp(std::string url) {
auto socket = co_await connect(url); // 等待连接建立
co_await socket.send("GET / HTTP/1.1\r\n"); // 等待发送完成
auto response = co_await socket.recv(); // 等待接收数据
std::cout << response;
}
你看,代码从上往下读,逻辑清晰。但底层其实是非阻塞的,不会浪费 CPU 空转。
4.2 游戏开发:帧率友好的异步逻辑
在游戏里,你经常需要「等几秒再执行某个动作」。用线程?太浪费。用定时器回调?代码分散。用协程?完美。
Task showDialogAfterDelay() {
co_await sleep_for(3s); // 等待3秒,不阻塞主线程
showDialog("欢迎回来!");
}
我在做一个小游戏时,就用协程实现了 NPC 的对话逻辑。每个 NPC 有自己的协程,该说话时说话,该走路时走路,代码非常简洁。
4.3 流式数据处理:按需生成
前面提到的 co_yield 很适合做数据管道。比如从数据库逐行读取百万条记录,用协程生成器,每次只取一条,内存占用极低。
Generator<Record> readLargeTable() {
auto cursor = db.query("SELECT * FROM big_table");
while (cursor.next()) {
co_yield cursor.current(); // 吐出一条记录
}
}
五、知识体系总览
下面这张图,是我自己总结的协程知识结构。你可以把它当作一个「地图」,学完后再回来看,会更有感觉。
六、写在最后
协程是 C++20 最重磅的特性之一,没有「之一」。它让异步编程变得优雅、高效、可维护。我个人觉得,未来五年,协程会成为 C++ 后端开发的主流范式。
不过,我也得提醒你:协程的学习曲线不低。尤其是 promise_type、awaitable 这些底层概念,需要花时间消化。别急,慢慢来。我当初也是看了好几遍标准文档,才真正搞明白。
嗯,今天就聊到这里。如果你在实际项目中遇到了协程相关的问题,欢迎交流。记住:协程不是银弹,但在合适的场景下,它是一把利器。