Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

现代 C++ / Modern C++

1. 现代 C++ 总览:不是特性清单,而是表达能力 / Modern C++ Overview

2. 移动、转发与值类别:std::move 为什么不移动 / Move, Forward, and Value Categories

3. 类型推导、初始化与 constexpr / Type Deduction, Initialization, and constexpr

4. 模板、实例化与 SFINAE / Templates, Instantiation, and SFINAE

5. concept 与约束:把模板要求写在接口上 / Concepts and Constraints

6. 异常、RAII 与 noexcept:失败路径也要清理 / Exceptions, RAII, and noexcept

7. lambda、捕获与生命周期:回调不是免费午餐 / Lambdas, Captures, and Lifetime

8. callable wrapper 与 std::invoke:怎样保存和调用行为 / Callable Wrappers and std::invoke

9. 词汇类型与错误建模:让接口自己说话 / Vocabulary Types and Error Modeling

10. 协程与异步抽象:先理解状态机 / Coroutines and Async Abstractions

本页目录

协程与异步抽象:先理解状态机 / Coroutines and Async Abstractions ​

C++ 协程最容易被误解成“轻量线程”。

更准确地说,协程让一个函数可以暂停,把状态保存起来,以后再恢复执行。它本身不等于并发,也不自动创建线程。

1. 普通函数的状态在调用期间存在 ​

普通函数:

cpp
void f() {
    int x = 1;
    int y = x + 1;
}
1
2
3
4

调用时通常有栈帧:

text
call f()
  |
  v
+------------------+
| frame of f       |
| x = 1            |
| y = 2            |
+------------------+
1
2
3
4
5
6
7
8

函数返回后:

text
frame of f is gone
x and y lifetime ended
1
2

普通函数不能执行到一半,把控制权交回去,过一会儿再从原位置继续。

2. 协程需要保存跨暂停点状态 ​

协程看起来像:

cpp
task f() {
    int x = 1;
    co_await something;
    use(x);
}
1
2
3
4
5

如果 co_await 暂停了,f() 已经把控制权交回调用者。

但以后恢复时,还要继续使用 x。

所以 x 不能只依赖普通栈帧。

编译器会把需要跨暂停点保存的状态放进协程帧。

text
coroutine frame
+------------------------------+
| promise object               |
| saved local variables        |
| current suspend point        |
| awaiter state                |
| bookkeeping                  |
+------------------------------+
1
2
3
4
5
6
7
8

协程帧通常在动态存储中,也可能被编译器优化掉。

重点是:

text
协程暂停后,状态必须保存在能活到恢复时的地方。
1

3. 协程不是自动异步 ​

有 co_await 不等于自动开线程。

协程只提供暂停和恢复机制。

真正把恢复动作接到事件循环、线程池、I/O 完成通知上的,是 awaiter 或框架。

text
coroutine reaches co_await
  |
  v
awaiter decides whether ready
  |
  +-- ready
  |     continue immediately
  |
  +-- not ready
        save coroutine handle
        suspend
        resume later by scheduler/event loop
1
2
3
4
5
6
7
8
9
10
11
12

如果没有调度器或 awaiter 逻辑,协程不会凭空异步运行。

4. 最小协程骨架 ​

一个能让编译器识别的协程返回类型,需要和 promise_type 配合。

cpp
#include <coroutine>
#include <exception>

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 hello() {
    co_return;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

这段代码不实用,但能说明协议。

编译器看到 hello() 里有 co_return,会把它当成协程,并寻找:

text
task::promise_type
1

promise_type 定义了协程和返回对象之间的约定。

5. co_await 的三个关键步骤 ​

一个 awaiter 通常涉及三个动作:

text
await_ready
  是否已经完成,不需要暂停?

await_suspend
  如果要暂停,拿到协程句柄后怎么安排恢复?

await_resume
  恢复后把什么结果交给 co_await 表达式?
1
2
3
4
5
6
7
8

流程:

text
co_await expr
  |
  v
get awaiter
  |
  v
await_ready()
  |
  +-- true -> await_resume(), continue
  |
  +-- false
        |
        v
     await_suspend(handle)
        |
        v
     coroutine suspended
        |
        v
     resume later
        |
        v
     await_resume()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这就是为什么协程更像“可暂停函数 + 状态机转换”。

6. 生命周期问题仍然存在 ​

协程能保存自己的局部状态。

但它不会自动延长外部对象生命周期。

危险例子:

cpp
task use_name(std::string_view name) {
    co_await something;
    use(name);
}
1
2
3
4

如果 name 观察的是调用者的临时字符串:

text
caller creates temporary string
  |
  v
use_name stores string_view
  |
  v
coroutine suspends
  |
  v
temporary string destroyed
  |
  v
coroutine resumes
  |
  v
string_view is dangling
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

协程帧保存了 string_view。

但 string_view 本身不拥有字符。

所以协程尤其要关注:

text
保存的是值,还是观察窗口?
观察对象会不会跨暂停点失效?
协程帧由谁拥有?
何时销毁?
1
2
3
4

7. 协程适合什么 ​

协程适合表达:

text
异步 I/O
生成器
复杂状态机
需要暂停和恢复的流程
1
2
3
4

例如异步 I/O 的直觉是:

text
发起读取
  |
  v
暂停当前协程
  |
  v
事件循环等待 I/O 完成
  |
  v
恢复协程
  |
  v
继续处理数据
1
2
3
4
5
6
7
8
9
10
11
12
13

这能把回调嵌套写成更接近顺序代码的形式。

但协程不适合用来逃避:

text
对象生命周期设计
错误传播设计
取消机制设计
调度器设计
1
2
3
4

这些仍然需要框架或程序员明确处理。

8. 本篇总结 ​

协程核心模型:

text
ordinary function
  |
  v
state lives in stack frame during call
  |
  v
return destroys frame

coroutine
  |
  v
state that crosses suspension lives in coroutine frame
  |
  v
co_await may suspend
  |
  v
scheduler or awaiter resumes later
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

理解协程时,先别急着背所有关键字。

先问:

text
哪里会暂停?
暂停时哪些变量必须保存?
协程帧由谁拥有?
什么时候恢复?
什么时候销毁?
跨暂停点保存的是拥有对象,还是非拥有 view/reference?
1
2
3
4
5
6

这些问题回答清楚,co_await、promise_type、awaiter 才会逐渐有意义。

最后更新于:

Pager
上一篇9. 词汇类型与错误建模:让接口自己说话 / Vocabulary Types and Error Modeling

持续记录,持续成长

Copyright © Tidenflow