一、协程是什么?我当年也困惑过

说实话,我第一次接触 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_alwaysstd::suspend_never,前者总是暂停,后者从不暂停。

小技巧:如果你想让协程在某个条件满足时才恢复,可以自己实现一个 Awaitable。比如等待一个 socket 可读,或者等待一个锁释放。

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_returnreturn 不能混用。一旦函数体内出现了 co_awaitco_yieldco_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 协程知识体系 协程 (Coroutines) co_await co_yield co_return 等待一个异步操作 暂停,等结果回来再继续 生成一个值 暂停,调用者取走值 结束协程 返回结果并终止 网络编程 · 文件I/O · 定时器 游戏开发 · 流式数据 · 生成器 对比:协程(用户态协作式) vs 线程(内核态抢占式)

六、写在最后

协程是 C++20 最重磅的特性之一,没有「之一」。它让异步编程变得优雅、高效、可维护。我个人觉得,未来五年,协程会成为 C++ 后端开发的主流范式。

不过,我也得提醒你:协程的学习曲线不低。尤其是 promise_typeawaitable 这些底层概念,需要花时间消化。别急,慢慢来。我当初也是看了好几遍标准文档,才真正搞明白。

嗯,今天就聊到这里。如果你在实际项目中遇到了协程相关的问题,欢迎交流。记住:协程不是银弹,但在合适的场景下,它是一把利器


公众号:蓝海资料掘金营,微信deep3321