C++20 协程入门与实践

C++20 协程入门与实践

时间:2026/04/09

关键词:co_awaitco_returnco_yieldpromise_type、挂起点、异步流程、generator
核心目标:理解协程为什么不是“更轻的线程”,而是一种把异步控制流写成顺序代码的语言机制。


1. 协程到底解决什么问题

传统异步代码常见两个问题:

  • 回调层层嵌套
  • 状态机代码难写难读

协程的核心价值是:

  • 把“会暂停、稍后恢复”的流程写成看起来接近顺序代码的形式

所以它更像:

  • 语言级状态机生成器

而不是线程替代品。


2. 协程不是线程

这点最重要。

协程:

  • 默认不并行
  • 默认不自动切线程
  • 只是能挂起和恢复

线程:

  • 是操作系统调度实体
  • 真正涉及并行执行

所以:

  • 协程解决的是“控制流组织”
  • 线程解决的是“执行资源”

3. 三个关键字

3.1 co_await

等待某个可等待对象,并可能挂起当前协程。

3.2 co_return

从协程返回结果。

3.3 co_yield

常用于生成器场景,逐个产出值。


4. 编译器视角下协程发生了什么

当函数里出现协程关键字后,它会被编译器改写成:

  • 一个协程状态对象
  • 一个 promise_type
  • 若干挂起点
  • 一个恢复入口

也就是说,协程本质上是:

  • 编译器自动帮你拆出来的状态机

5. promise_type 是什么

如果一个返回类型想作为协程返回对象,就需要配套定义:

  • promise_type

它负责描述:

  • 协程如何创建
  • 如何返回值
  • 初始/最终是否挂起
  • 异常怎么处理

这是 C++ 协程里最“底层”的部分。


6. 一个最小协程返回类型骨架

1
2
3
4
5
6
7
8
9
10
11
#include <coroutine>

struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};

这样一个返回 Task 的函数就可以写成协程。


7. co_await 的直觉

写:

1
co_await something;

编译器会尝试把 something 变成一个 awaitable,并调用:

  • await_ready()
  • await_suspend()
  • await_resume()

可以粗略理解为:

  1. 先问要不要挂起
  2. 如果挂起,如何安排恢复
  3. 恢复后返回什么结果

8. generator 场景为什么适合协程

例如想逐个生成值:

  • 传统写法要手工保存状态
  • 协程可以自然写成“产出一个,暂停,再继续”

这正是 co_yield 最直观的用法。

所以协程特别适合:

  • lazy sequence
  • parser
  • pipeline
  • 异步流

9. 协程最常见的工程用途

9.1 异步 I/O

把:

  • 发请求
  • 等待回包
  • 继续处理

写成线性流程。

9.2 生成器

按需逐个产出数据。

9.3 Actor / 任务系统

用协程表达暂停与恢复点。


10. 协程为什么很容易“看起来简单,实际上不简单”

因为源码很线性,但真实问题仍然存在:

  • 生命周期谁管
  • 在哪条线程恢复
  • 恢复时机谁触发
  • 异常怎么传播
  • 取消怎么处理

也就是说:

  • 协程简化的是控制流表达
  • 没有消灭异步系统的本质复杂度

11. 一个最重要的工程问题:恢复在哪发生

协程本身不决定线程。
真正决定“恢复在哪个执行器/事件循环/线程池”的,是 awaitable 或运行时框架。

所以写协程框架时一定要搞清楚:

  • 谁调 resume
  • 什么时候调
  • 在哪调

12. 常见坑

12.1 把协程当轻量线程

这是最常见误解。

12.2 忽略返回对象和 promise 生命周期

协程帧如果没人管理,容易泄漏或悬空。

12.3 只学语法,不理解恢复机制

这样很快就会在真实异步项目里迷路。

12.4 在协程里捕获悬空引用

因为协程可能挂起很久,引用生命周期尤其要小心。


13. 一页总结

协程最重要的理解链是:

  1. 协程不是线程,而是可挂起函数
  2. 编译器会把协程改写成状态机
  3. co_await 的核心是等待、挂起和恢复
  4. 真正工程难点在生命周期、调度器和恢复时机

如果只记一句:

协程的价值是把异步流程写直,而不是把并发问题自动解决掉。