编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • 进程地址空间布局
      • 对象内存布局原理
      • 引用与指针本质
      • this指针与成员函数
      • 虚函数表深度剖析
      • 多重继承内存模型
      • 内存对齐与缓存行
      • 内存分配器演进史
      • 五大值类别详解
      • 右值引用与移动语义
      • 完美转发与引用折叠
      • 类型推导三大规则
      • 类型转换与隐式构造
      • const与volatile真相
      • RTTI与dynamic_cast
      • 类型擦除技术原理
      • 模板实例化机制
      • 模板特化与偏特化
      • SFINAE与enable_if
      • 可变参数模板原理
      • constexpr编译期计算
      • Concepts深度剖析
      • 元编程模板技巧
      • Modules模块化设计
      • RAII的设计哲学
      • 对象构造与析构
      • 拷贝与移动控制
      • unique_ptr原理剖析
      • shared_ptr底层剖析
      • weak_ptr与this增强
      • 五种存储期管理
      • vector扩容真相
      • deque分段连续设计
      • list与forward_list
      • 关联容器红黑树
      • 哈希容器深度剖析
      • 迭代器五大类别
      • STL算法设计哲学
      • Allocator分配器机制
      • C++内存模型基石
      • 六大内存序详解
      • atomic原子操作原理
      • mutex与条件变量
      • thread与jthread机制
      • 异步编程future家族
      • 无锁数据结构设计
      • 协程coroutine原理
        • 1. 案例引入
          • 1.1 Generator 忘记 co_yield——空生成器的静默崩
          • 1.2 promise_type 提前析构——协程栈帧的悬挂地狱
          • 1.3 八个待解疑问
        • 2. 架构概览
          • 2.1 协程三角——promise_type / awaiter / handle 的职责划分
          • 2.2 为何这么切
          • 2.3 栈less协程的内存秘密——协程帧在堆上、局部变量在帧里
        • 3. promise_type——协程的调度中心
          • 3.1 编译器要求的五(或七)个接口函数
          • 3.2 initial_suspend——协程第一行前停在哪
          • 3.3 finalsuspend——最后一个 coawait 之后去哪
          • 3.4 yieldvalue 与 returnvalue 的语义差异
          • 3.5 getreturnobject——协程和调用方的唯一接口
          • 3.6 unhandled_exception——协程异常的最后一站
        • 4. awaiter 与 co_await——暂停和恢复的原子操作
          • 4.1 awaiter 三函数:awaitready / awaitsuspend / await_resume
          • 4.2 co_await 的完整编译器变换——从一行代码到状态机
          • 4.3 suspendalways vs suspendnever——初始暂停与最终暂停的设计意图
          • 4.4 自定义 awaiter——把一个 IO 操作变成可暂停的协程点
        • 5. coroutine_handle——协程帧的物理操作杆
          • 5.1 源码——只是一个 void* 的薄包装
          • 5.2 resume——从暂停点继续执行的完整路径
          • 5.3 destroy——析构协程帧、释放堆内存
          • 5.4 done——检查协程是否已结束
          • 5.5 address——获取帧地址用于调试和自定义调度
        • 6. coyield 与 coreturn——两种不同的结束方式
          • 6.1 coyield 的本质——coawait promisetype.yieldvalue() 的语法糖
          • 6.2 coreturn 的 void 与有值版本——returnvalue vs return_void
          • 6.3 生成器模式——co_yield + for 循环的完整协程版本
        • 7. 协程栈帧的完整布局——编译器生成的隐藏结构
          • 7.1 函数签名到帧结构变换
          • 7.2 局部变量、参数、暂停点的跨帧保存
          • 7.3 与内核/绿色线程的有栈协程对比——栈less 有栈的本质区别
        • 8. 异步协程模式——不阻塞等待而是 co_await
          • 8.1 单线程异步调度器——一个最简单的 scheduler 实现
          • 8.2 与 std::future/std::thread/std::async 的对比——为什么协程是更好的异步抽象
          • 8.3 异步 generator——流式处理的协程范式
        • 9. 常见陷阱与反模式
          • 9.1 coroutine_handle::destroy 忘记调用——内存泄露
          • 9.2 promisetype 中取引用的悬挂——getreturn_object 返回引用指向已析构的帧
          • 9.3 在 final_suspend 中 resume 自己——无限递归
          • 9.4 coawait 一个临时对象——awaitsuspend 返回时临时已析构
          • 9.5 生成器的迭代器失效——在迭代期间修改协程帧
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 一次 co_await 的完整生命周期——从暂停到恢复
          • 10.3 设计哲学回扣
          • 10.4 速查表合集
      • 翻译单元与预处理
      • 编译期符号生成
      • 链接器工作原理
      • ODR规则与陷阱
      • 动态库与符号可见性
      • C++ ABI兼容性
      • LTO与PGO优化
      • 异常机制底层原理
      • Ranges革命与管道
      • format与print体系
      • UB未定义行为图鉴
      • C++设计哲学回望
      • 写作模板
    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 专栏博客
杨充
2026-06-06
目录

协程coroutine原理

# 47.协程coroutine原理

# 目录介绍

  • 1. 案例引入
    • 1.1 Generator 忘记 co_yield——空生成器的静默崩
    • 1.2 promise_type 提前析构——协程栈帧的悬挂地狱
    • 1.3 八个待解疑问
  • 2. 架构概览
    • 2.1 协程三角——promise_type / awaiter / handle 的职责划分
    • 2.2 为何这么切
    • 2.3 栈less协程的内存秘密——协程帧在堆上、局部变量在帧里
  • 3. promise_type——协程的调度中心
    • 3.1 编译器要求的五(或七)个接口函数
    • 3.2 initial_suspend——协程第一行前停在哪
    • 3.3 final_suspend——最后一个 co_await 之后去哪
    • 3.4 yield_value 与 return_value 的语义差异
    • 3.5 get_return_object——协程和调用方的唯一接口
    • 3.6 unhandled_exception——协程异常的最后一站
  • 4. awaiter 与 co_await——暂停和恢复的原子操作
    • 4.1 awaiter 三函数:await_ready / await_suspend / await_resume
    • 4.2 co_await 的完整编译器变换——从一行代码到状态机
    • 4.3 suspend_always vs suspend_never——初始暂停与最终暂停的设计意图
    • 4.4 自定义 awaiter——把一个 IO 操作变成可暂停的协程点
  • 5. coroutine_handle——协程帧的物理操作杆
    • 5.1 源码——只是一个 void* 的薄包装
    • 5.2 resume——从暂停点继续执行的完整路径
    • 5.3 destroy——析构协程帧、释放堆内存
    • 5.4 done——检查协程是否已结束
    • 5.5 address——获取帧地址用于调试和自定义调度
  • 6. co_yield 与 co_return——两种不同的结束方式
    • 6.1 co_yield 的本质——co_await promise_type.yield_value() 的语法糖
    • 6.2 co_return 的 void 与有值版本——return_value vs return_void
    • 6.3 生成器模式——co_yield + for 循环的完整协程版本
  • 7. 协程栈帧的完整布局——编译器生成的隐藏结构
    • 7.1 从函数签名到帧结构的编译器变换过程
    • 7.2 局部变量、参数、暂停点的跨帧保存
    • 7.3 与内核/绿色线程的有栈协程对比——栈less 有栈的本质区别
  • 8. 异步协程模式——不阻塞等待而是 co_await
    • 8.1 单线程异步调度器——一个最简单的 scheduler 实现
    • 8.2 与 std::future/std::thread/std::async 的对比——为什么协程是更好的异步抽象
    • 8.3 异步 generator——流式处理的协程范式
  • 9. 常见陷阱与反模式
    • 9.1 coroutine_handle::destroy 忘记调用——内存泄露
    • 9.2 promise_type 中取引用的悬挂——get_return_object 返回引用指向已析构的帧
    • 9.3 在 final_suspend 中 resume 自己——无限递归
    • 9.4 co_await 一个临时对象——await_suspend 返回时临时已析构
    • 9.5 生成器的迭代器失效——在迭代期间修改协程帧
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 一次 co_await 的完整生命周期——从暂停到恢复
    • 10.3 设计哲学回扣
    • 10.4 速查表合集

# 1. 案例引入

# 1.1 Generator 忘记 co_yield——空生成器的静默崩

某数据处理框架用 C++20 协程写了一个惰性生成器,用于流式读取文件并逐行处理。在模板化阶段,某次重构后生成器突然不产出任何数据:

// ====== 事故代码 V1:没有 co_yield 的生成器——编译器不报错 ======
generator<int> range(int start, int end) {
    for (int i = start; i < end; ++i) {
        // ❌ 忘记写 co_yield——协程体里没有任何协程关键字
        // 编译器把这段代码当成普通函数编译——没有协程状态机
        // → 调用方迭代——generator 的 begin() 返回 end()——永远不产出数据
    }
    co_return;  // 这个 co_return 是多余的——编译器在前面的 for 里没看到协程操作
}
1
2
3
4
5
6
7
8
9

编译通过 + 运行不报错 + 不产出数据——三元最恐怖的 bug。

根因:C++20 的协程检测是基于函数体中是否存在 co_await / co_yield / co_return 三者之一。如果协程体里没有任何协程关键字——编译器直接走普通函数路径——不生成协程帧。然而写代码的人以为自己在写协程——类型签名的 generator<int> 也暗示了这一点。

# 1.2 promise_type 提前析构——协程栈帧的悬挂地狱

同一个框架的异步 RPC 模块用协程做请求-响应调度。在协程挂起后、恢复前,框架持有的 coroutine_handle 变成了悬空指针:

// ====== 事故代码 V2:promise_type 析构 → coroutine_handle 悬垂 ======
struct Task {
    struct promise_type {
        Task get_return_object() {
            return Task{coroutine_handle<promise_type>::from_promise(*this)};
            // ⚠️ *this 是 promise_type——在协程帧内部
            //    协程帧什么时候析构?如果 ~Task 没有调 handle.destroy()——
            //    协程帧在 promise_type 所在的内存被回收后──被重新分配!
        }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() { return {}; }  // ⚠️ suspend_never!
        void return_void() {}
        void unhandled_exception() {}
    };
    coroutine_handle<promise_type> handle_;

    ~Task() {
        // ❌ 忘记 handle_.destroy()!
    }
};

// 在调用方:
Task t = async_operation();   // ① 协程启动(initial_suspend = never → 立即执行)
// ② 协程遇到 co_await → 暂停
// ③ 协程帧在堆上——等待被恢复
// ④ ... 某个时刻 t 析构(离开作用域)→ ~Task 不调 destroy
// ⑤ 协程帧变成「活着的但没人引用的幽灵」
//    恢复协程的代码仍然持有旧的 handle_ → 指向已被回收(复用)的堆内存
//    → resume() 读到垃圾值 → SIGSEGV
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
29

根因链条:

  1. final_suspend = suspend_never → 协程在最后一个 co_await 之后自动析构协程帧
  2. 但如果协程在 co_await 上暂停了(未到 final_suspend)→ 协程帧仍在堆上
  3. ~Task 不调 destroy → 协程帧泄漏或变成悬垂指针
  4. 后续恢复请求 → 操作已释放的内存

# 1.3 八个待解疑问

① 协程的三个关键字 co_await/co_yield/co_return 分别对应什么样的暂停/恢复语义?  → 第 4 / 第 6 章
② promise_type 的 5-7 个接口函数各在什么时机被谁调用?编译器怎么插桩?          → 第 3 章
③ awaiter 的 await_ready/await_suspend/await_resume 三个函数的完整调用顺序?  → 第 4 章
④ coroutine_handle 为什么只是一个 void*?它是怎么 resume/destroy 协程帧的?     → 第 5 章
⑤ 栈less协程是"函数停在哪里,栈帧就分配到哪里"——局部变量都去哪了?               → 第 7 章
⑥ 生成器怎么写?co_yield 背后是什么语法变换?                                   → 第 6.3 章
⑦ 协程怎么替代 hand-rolled 异步状态机?比 future/thread 哪里强?                → 第 8 章
⑧ final_suspend 的 suspend_always vs suspend_never 怎么选?                    → 第 3.3 / 第 10 章
1
2
3
4
5
6
7
8

# 2. 架构概览

# 2.1 协程三角——promise_type / awaiter / handle 的职责划分

                        ┌──────────────────────┐
                        │   compiler-generated  │
                        │    协程帧 (堆上)       │
                        │                      │
                        │  ┌─────────────────┐ │
                        │  │  promise_type   │ │ ← 协程的"寄存器"—保存状态/值/异常
                        │  │  (调度控制中心)  │ │
                        │  └────────▲────────┘ │
                        │           │           │
                        │           │ get_return│
                        │           │ _object() │
                        │           │           │
                        │  ┌────────┴────────┐  │
                        │  │  coroutine_     │  │ ← 帧的"操作杆"
                        │  │  handle (void*) │  │   resume/destroy/done
                        │  └────────▲────────┘  │
                        │           │           │
                        │  ┌────────┴────────┐  │
                        │  │  awaiter        │  │ ← 暂停/恢复的"开关"
                        │  │  await_ready    │  │   co_await 表达式展开处
                        │  │  await_suspend  │  │
                        │  │  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
24

三角的分工:

  • promise_type:控制「协程生什么」——生成器产出的值、协程的返回值、异常、启动行为
  • awaiter:控制「协程停在哪、怎么恢复」——暂停条件、恢复调度
  • coroutine_handle:协程帧的物理指针——resume/destroy 是唯二的操作

# 2.2 为何这么切

疑惑:普通函数不能暂停吗?为什么需要协程?

论证——普通函数和协程的本质差异:

普通函数:
  调用 → 压栈 → 执行 → 返回 → 弹栈 → 函数结束
  中间不能暂停——一旦返回,栈帧消失、局部变量消失

协程函数(C++20 栈less):
  调用 → 分配协程帧(堆) → 执行 → co_await → 暂停(帧保留)
       → 恢复 → 继续执行 → co_return → 帧销毁
  中间可暂停任意多次——每次暂停时帧仍在堆上,下次恢复时继续

为什么叫「栈less」——和「有栈协程」的对比:
  有栈协程(如 Go goroutine、Lua coroutine):
    每个协程有自己独立的栈——切换时切整个栈指针
  栈less协程(C++20、Rust):
    协程帧在堆上——暂停时行帧保留在堆中
    不需要独立的栈——比有栈协程更轻量(不需要 1-8MB 栈分配)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2.3 栈less协程的内存秘密——协程帧在堆上、局部变量在帧里

generator<int> sequence() {
    int a = 10;                              // ① 局部变量
    co_yield a;                              // ② 暂停点
    int b = a + 20;                          // ③ 跨暂停点的局部变量
    co_yield b;
    co_return;
}
1
2
3
4
5
6
7

编译器变换后的协程帧布局:

协程帧 (堆上,编译器生成的结构):

┌─────────────────────────────────────────────┐
│  promise_type (promise)                      │  ← 内嵌的 promise_type
│  - 状态: 当前在第几个暂停点 (resume point)   │
│  - 保存的 value (yield 的值)                 │
├─────────────────────────────────────────────┤
│  int a;      // 局部变量——从栈搬进帧        │  ← 不需要跨暂停点的变量也会在帧中
│  int b;      // 局部变量——跨暂停点          │
├─────────────────────────────────────────────┤
│  其他编译器生成的状态:                       │
│  - resume point index (0/1/2)                │
│  - exception_ptr (如有未处理的异常)          │
└─────────────────────────────────────────────┘

sizeof(协程帧) ≈ sizeof(promise_type) + sizeof(所有局部变量) + overhead(~40B)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

关键理解:协程函数中的所有局部变量都被搬进协程帧——不管它们是否需要跨暂停点。这是编译器的一律处理策略——因为编译器必须保证在 co_await 暂停时帧中能恢复全部状态。


# 3. promise_type——协程的调度中心

# 3.1 编译器要求的五(或七)个接口函数

struct promise_type {
    // ===== 必须实现的 5 个 =====

    // ① 创建返回值对象——调用方拿到的不是协程帧,是包装对象
    Task get_return_object();

    // ② 第一个暂停点之前——suspends at beginning or runs immediately
    std::suspend_always initial_suspend();  // 或 suspend_never

    // ③ 最后一个暂停点之后——协程结束时
    std::suspend_always final_suspend() noexcept;  // 或 suspend_never

    // ④ co_yield a → 编译器调用 yield_value(a)
    std::suspend_always yield_value(int val);

    // ⑤ co_return; (无值返回)
    void return_void();

    // ===== 可选——如果 co_return v (有值) =====
    // ⑥ co_return v;
    void return_value(int val);

    // ===== 必须 =====
    // ⑦ 协程体内未捕获的异常
    void unhandled_exception();
};
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

编译器的插桩时机——协程函数被编译器展开后的调用序列:

编译器变换后:

generator&lt;int> sequence() {
    // === 编译器插入——阶段 1:创建协程帧 ===
    frame = operator new(sizeof(coroutine_frame));
    frame->promise = promise_type{};
    frame->a = 10;        // 搬移局部变量到帧

    // === 编译器插入——阶段 2:get_return_object ===
    auto return_obj = frame->promise.get_return_object();

    // === 编译器插入——阶段 3:initial_suspend ===
    co_await frame->promise.initial_suspend();
    // 如果 suspend_always → 暂停,返回 return_obj 给调用方
    // 如果 suspend_never → 继续执行协程体

    // === 协程体(原代码) ===
    // int a = 10;         ← 已在阶段 1 搬进帧
    co_yield frame->a;     ← 展开为 co_await promise.yield_value(a)
    int b = a + 20;        ← b 也在帧中
    co_yield b;
    co_return;              ← 展开为 promise.return_void(); goto final_suspend;

final_suspend:
    // === 编译器插入——final_suspend ===
    co_await frame->promise.final_suspend();
    // 如果 suspend_always → 暂停(由调用方 destroy)
    // 如果 suspend_never → 自动析构帧
}
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
29

# 3.2 initial_suspend——协程第一行前停在哪

suspend_always 协程:
  调用方拿到 return_obj 后——协程还未执行
  需要显式 resume → 协程从头开始执行
  适用场景:惰性求值、需要回调注册之后才启动

suspend_never 协程:
  调用方拿到 return_obj 时——协程已执行到第一个 co_await
  不需要显式 resume——"热启动"
  适用场景:异步任务——启动后立即执行直到遇到 IO
1
2
3
4
5
6
7
8
9

Generator 必须 suspend_always、异步 Task 通常 suspend_never。

# 3.3 final_suspend——最后一个 co_await 之后去哪

这是协程生命周期管理的最关键设计点——直接决定了谁负责析构协程帧:

final_suspend = suspend_always:
  协程在结束时「停住」——不自己析构帧
  调用方需要通过 handle.destroy() 手动析构帧
  好处:可以在 final_suspend 后安全地读取 promise 中的返回值/异常
        (因为帧还活着、promise 还活着)
  这是推荐模式——和 RAII 兼容

final_suspend = suspend_never:
  协程在结束时「不停」——自动析构帧
  调用方不需要手动 destroy
  坏处:在 co_return 之后无法访问 promise 的数据
        (帧已析构——promise 也随之析构)
1
2
3
4
5
6
7
8
9
10
11
12

案例 1.2 的根因:final_suspend = suspend_never——协程暂停在中间的 co_await 时帧还活着,但一旦协程结束(不管是被 resume 完成还是异常)——帧自动析构。调用方持有的 coroutine_handle 变成悬垂指针。

# 3.4 yield_value 与 return_value 的语义差异

// co_yield val → 编译器展开:
//   promise.yield_value(val);
//   // 暂停——控制权回到调用方

// co_return val → 编译器展开:
//   promise.return_value(val);
//   // 跳到 final_suspend

// 关键差异:co_yield 之后协程还活着——co_return 之后协程走向结束
1
2
3
4
5
6
7
8
9

# 3.5 get_return_object——协程和调用方的唯一接口

struct Task::promise_type {
    Task get_return_object() {
        return Task{coroutine_handle<promise_type>::from_promise(*this)};
        // from_promise:把 promise 的地址转成协程帧的地址
        // → 返回 coroutine_handle——调用方通过它来控制协程
    }
};
1
2
3
4
5
6
7

为什么返回值类型不叫 coroutine_handle 而是 Task/Generator? 因为返回对象可以做 RAII——在析构时自动 handle.destroy()。这就是为什么 generator<T> 是一个范例——它把协程帧的生命周期用 RAII 包裹起来。

# 3.6 unhandled_exception——协程异常的最后一站

void unhandled_exception() {
    // std::current_exception() 捕获当前未处理的异常
    // 存储到 promise 的某个成员——让 get_return_object 返回的对象可以传播它
    // 或者在 rethrow 之前存储到 promise 中供调用方检查

    // 典型实现:把异常存进 promise,在 final_suspend 后抛回给调用方
    exception_ = std::current_exception();
}
1
2
3
4
5
6
7
8

# 4. awaiter 与 co_await——暂停和恢复的原子操作

# 4.1 awaiter 三函数:await_ready / await_suspend / await_resume

struct Awaiter {
    // ① 检查是否「已有结果」「不需要暂停」
    bool await_ready() const {
        return data_ready_;           // true → 跳过暂停(和同步调用一样)
    }

    // ② 暂停——协程在这里挂起
    //    可以返回 void/bool/coroutine_handle
    void await_suspend(coroutine_handle<> h) {
        // h 是当前协程的 handle——保存它,供恢复时使用
        scheduler_->schedule(h);      // 注册到调度器
    }

    // ③ 恢复——协程被 resume 后,这里拿到结果
    Data await_resume() const {
        return data_;                 // 返回给 co_await 表达式的值
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

关键:co_await awaiter 的结果是 awaiter.await_resume() 的返回值——不是 awaiter 本身。

# 4.2 co_await 的完整编译器变换——从一行代码到状态机

// 原始代码:
Data d = co_await async_read(sock);

// ↓ 编译器展开为:

auto&& awaiter = async_read(sock);        // ① 获取 awaiter

if (!awaiter.await_ready()) {             // ② 是否需要暂停?
    // === 暂停协程 ===
    // 保存当前执行点(resume_point = N)
    // 保存所有寄存器

    awaiter.await_suspend(handle);         // ③ 通知 awaiter——协程暂停了
    // await_suspend 返回后——协程已暂停
    // 控制权回到调用方/resume 者

    // === 恢复后——从 await_resume 开始 ===
}

Data d = awaiter.await_resume();          // ④ 取结果(或在暂停前已取——看实现)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

await_suspend 的三种返回类型——决定控制流转给谁:

返回类型 语义 控制流转给
void 暂停——控制权返回到调用方(或上次 resume 者) 调用方
bool true: 暂停(同 void);false: 不暂停(立即 resume) 调用方 / 当前协程
coroutine_handle<> 暂停——控制权转给另一个协程(对称转移) 另一个协程

对称转移是协程性能的关键优化——直接从一个协程跳转到另一个——不经过调用方的中间栈帧。

# 4.3 suspend_always vs suspend_never——初始暂停与最终暂停的设计意图

struct suspend_always {
    bool await_ready() const noexcept { return false; }          // 永远暂停
    void await_suspend(coroutine_handle<>) const noexcept {}     // 不做调度
    void await_resume() const noexcept {}                        // 无返回值
};

struct suspend_never {
    bool await_ready() const noexcept { return true; }           // 永远不暂停
    void await_suspend(coroutine_handle<>) const noexcept {}
    void await_resume() const noexcept {}
};
1
2
3
4
5
6
7
8
9
10
11

initial_suspend 用 suspend_always vs suspend_never 的语义已在 3.2 展开。核心:懒启动 vs 立即启动。

# 4.4 自定义 awaiter——把一个 IO 操作变成可暂停的协程点

struct IoAwaiter {
    int sock_;
    Data buffer_;

    bool await_ready() {
        // 非阻塞检查——如果数据已在 kernel buffer 中
        int n = recv(sock_, &buffer_, sizeof(buffer_), MSG_DONTWAIT);
        if (n > 0) { already_ = true; return true; }   // 已有数据——不暂停
        if (n == 0) { eof_ = true;  return true; }     // EOF——不暂停
        return false;                                    // 暂无数据——暂停
    }

    void await_suspend(coroutine_handle<> h) {
        // 注册到 epoll——数据可读时 → 恢复 h
        epoll_ctl(epfd_, EPOLL_CTL_ADD, sock_,
                  &(epoll_event){EPOLLIN, {.ptr = h.address()}});
    }

    Data await_resume() {
        if (eof_) throw eof_error{};
        if (already_) return buffer_;                   // await_ready 时已读了
        recv(sock_, &buffer_, sizeof(buffer_), 0);      // 恢复时再读
        return buffer_;
    }
};

// 使用——像同步代码一样写异步 IO:
Data d = co_await IoAwaiter{sock};
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

# 5. coroutine_handle——协程帧的物理操作杆

# 5.1 源码——只是一个 void* 的薄包装

template <typename Promise = void>
struct coroutine_handle {
    void* ptr_;  // 指向协程帧的第一个字节

    // 从 promise 反向获取 handle
    static coroutine_handle from_promise(Promise& p) {
        // 编译器知道 promise 在帧中的偏移量
        // → ptr = &p - offset_of(promise)
        coroutine_handle h;
        h.ptr_ = static_cast<char*>(static_cast<void*>(&p)) - some_offset;
        return h;
    }

    // 从 void* 构造(from address() 的逆向)
    static coroutine_handle from_address(void* addr) {
        coroutine_handle h;
        h.ptr_ = addr;
        return h;
    }

    void* address() const { return ptr_; }

    explicit operator bool() const { return ptr_ != nullptr; }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

sizeof(coroutine_handle<>) = sizeof(void) = 8 字节。* 它不拥有协程帧——只是指向它。谁负责析构框架——这是 final_suspend 和 promise_type 配合的结果。

# 5.2 resume——从暂停点继续执行的完整路径

void resume() {
    // ① 检查协程是否已结束
    if (done()) return;  // 或抛异常

    // ② 恢复执行——从上次暂停的下一条指令开始
    //    内部:设置指令寄存器到 resume_point + 恢复寄存器 + 跳转
    resume_impl(ptr_);   // 编译器内建——对于特定平台
}
1
2
3
4
5
6
7
8

resume 做了什么——完全由编译器生成:每个协程帧的头部有一个 resume 函数指针——指向编译器生成的 __coroutine_resume 函数。这个函数做:

  1. 从帧中恢复寄存器
  2. 跳转到正确的 resume_point(协程状态机内的标签)
  3. 继续执行

# 5.3 destroy——析构协程帧、释放堆内存

void destroy() {
    // ① 析构帧中的所有局部变量(按构造的反序)
    // ② 析构 promise_type
    // ③ operator delete(ptr_)——释放帧内存
    destroy_impl(ptr_);
}
1
2
3
4
5
6

# 5.4 done——检查协程是否已结束

bool done() const {
    // 读取帧中的状态标志——是否到达 final_suspend 且 final_suspend 不会暂停
    return frame_is_done(ptr_);
}
1
2
3
4

done == true 之后——resume 和 destroy 都是 UB。

# 5.5 address——获取帧地址用于调试和自定义调度

void* addr = handle.address();
// 可以将帧地址传给 epoll 的 data.ptr、自定义调度器的事件等
epoll_event ev;
ev.data.ptr = handle.address();  // 当 IO 就绪时——从此地址恢复 coroutine_handle
1
2
3
4

# 6. co_yield 与 co_return——两种不同的结束方式

# 6.1 co_yield 的本质——co_await promise_type.yield_value() 的语法糖

// 原始协程:
generator<int> sequence() {
    co_yield 42;   // ①
    co_yield 84;   // ②
}

// ====== 编译器展开 ======
generator<int> sequence() {
    // ① co_yield 42 → 
    co_await promise.yield_value(42);
    // yield_value 返回 suspend_always → 暂停
    // 调用方从 generator 的迭代器中获取 42

    // ② co_yield 84 → 
    co_await promise.yield_value(84);
    // 同上
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

yield_value 的实现范例:

struct promise_type {
    int current_value_;

    std::suspend_always yield_value(int val) {
        current_value_ = val;    // 存值——供迭代器读取
        return {};               // 暂停——控制权回到调用方
    }
};
1
2
3
4
5
6
7
8

# 6.2 co_return 的 void 与有值版本——return_value vs return_void

// co_return; → promise.return_void(); → final_suspend
// co_return val; → promise.return_value(val); → final_suspend

// 一个 promise_type 只能实现 return_value 或 return_void 之一
// 不能同时实现——编译器在解析协程体时决定用哪个
1
2
3
4
5

# 6.3 生成器模式——co_yield + for 循环的完整协程版本

template <typename T>
struct generator {
    struct promise_type {
        T current_val_;

        generator get_return_object() {
            return generator{coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(T val) { current_val_ = val; return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    struct iterator {
        coroutine_handle<promise_type> h_;
        bool operator!=(std::default_sentinel_t) const { return !h_.done(); }
        iterator& operator++() { h_.resume(); return *this; }
        T operator*() const { return h_.promise().current_val_; }
    };

    coroutine_handle<promise_type> h_;
    iterator begin() { h_.resume(); return {h_}; }
    std::default_sentinel_t end() { return {}; }
    ~generator() { if (h_) h_.destroy(); }
};

// 使用——像 Python generator 一样简洁:
generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        auto next = a + b;
        a = b;
        b = next;
    }
}

for (int v : fibonacci()) {   // 惰性生成——只在需要时计算下一个值
    if (v > 1000) break;
    std::cout << v << '\n';
}
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 7. 协程栈帧的完整布局——编译器生成的隐藏结构

# 7.1 函数签名到帧结构变换

// 原始函数:
generator<int> range(int start, int end) {
    for (int i = start; i < end; ++i)
        co_yield i;
    co_return;
}
1
2
3
4
5
6

编译器变换后的帧——简化版:

struct __range_frame {
    // === 编译器生成的状态 ===
    void (*resume_fn)(__range_frame*);  // 恢复函数指针
    void (*destroy_fn)(__range_frame*); // 析构函数指针
    int resume_point;                    // 0=initial, 1=after co_yield, -1=done

    // === promise_type ===
    generator<int>::promise_type __promise;

    // === 形参(搬到帧上) ===
    int start;
    int end;

    // === 局部变量(搬到帧上) ===
    int i;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

resume 函数——编译器生成的 dispatch 代码:

void __range_resume(__range_frame* frame) {
    switch (frame->resume_point) {
    case 0: goto initial;
    case 1: goto after_yield;
    case -1: return;  // done
    }

initial:
    for (frame->i = frame->start; frame->i < frame->end; ++frame->i) {
        frame->__promise.current_val_ = frame->i;
        frame->resume_point = 1;
        return;  // 暂停——等待下一次 resume

after_yield:
        ;  // 继续 for 循环
    }
    frame->__promise.return_void();
    frame->resume_point = -1;
    // final_suspend
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

关键:每个暂停点对应 resume_point 的一个整数值。co_yield 在设置 resume_point 后 return——下一次 resume 从 after_yield 标签继续。

# 7.2 局部变量、参数、暂停点的跨帧保存

协程帧中所有需要在暂停之间存活的变量:
  ✓ 所有形参——通过值传递、或 move 到帧上
  ✓ 所有跨暂停点的局部变量——存放在帧中
  ✓ 所有临时对象——如果在 co_await 前构造、co_await 后仍需要——搬进帧

编译器优化——如果编译器能证明「变量不需要跨暂停点」:
  → 变量仍然放在帧上(一律策略)
    因为暂停点可能在循环/分支内部——编译器难做精细的活性分析

实例——20 行协程的帧大小 ~200-400 字节(包括编译器状态)
1
2
3
4
5
6
7
8
9
10

# 7.3 与内核/绿色线程的有栈协程对比——栈less 有栈的本质区别

维度 栈less协程 (C++20) 有栈协程 (Go goroutine, Lua)
栈分配 协程帧在堆上 ~200B 独立栈 1-8 MB
跨暂停点变量 编译器分析→搬进帧 自然在栈上
切换开销 resume = 分支跳转 + 恢复寄存器 (~5ns) 切栈 + 改栈指针 (~50ns)
堆碎片 多次协程创建 = 多次小分配 一次大分配
递归 不支持直接递归(帧需提前确定大小) 支持
嵌套协程 必须显式 co_await 子协程 原生 call
适用场景 异步 IO、生成器——轻量 通用用户态线程

# 8. 异步协程模式——不阻塞等待而是 co_await

# 8.1 单线程异步调度器——一个最简单的 scheduler 实现

struct Scheduler {
    std::queue<coroutine_handle<>> ready_queue_;

    void schedule(coroutine_handle<> h) {
        ready_queue_.push(h);
    }

    void run() {
        while (!ready_queue_.empty()) {
            auto h = ready_queue_.front();
            ready_queue_.pop();
            h.resume();  // 恢复协程——会再次 co_await → 暂停 → schedule
        }
    }
};

// 使用:
Scheduler sched;

auto task = [&]() -> Task {
    co_await async_read(sched, sock);    // 暂停——注册到 scheduler
    co_await async_write(sched, sock);
    co_return;
}();

sched.run();  // 单线程驱动所有协程——不需要锁
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

和传统的线程/回调对比:

  • 线程每次 IO 等待 = 一个线程阻塞 = 1MB 栈 + 上下文切换
  • 协程每次 IO 等待 = 一个协程帧暂停 = ~200B + resume(函数调用开销)

# 8.2 与 std::future/std::thread/std::async 的对比——为什么协程是更好的异步抽象

future 异步:
  auto f = std::async(work);
  int r = f.get();  // 阻塞等待——浪费一个线程

协程异步:
  int r = co_await async_work();  // 暂停——线程可以干别的
  // 协程帧在堆上保留状态——不需要整个线程
1
2
3
4
5
6
7
维度 thread+future 协程
IO 等待时的资源 1 个完整的线程(~1MB 栈) 协程帧 ~200B
上下文切换 内核态切(~3μs) 函数调用(~5ns)
逻辑流 回调 + split,难读 顺序代码——和同步一样
并发 真正的并行 单线程多路复用(或可多线程)
百万连接 不可能(百万线程) 可能(百万协程帧)

# 8.3 异步 generator——流式处理的协程范式

async_generator<Packet> read_packets(Socket sock) {
    while (true) {
        auto data = co_await async_read(sock);
        if (data.empty()) co_return;
        co_yield parse_packet(data);   // 惰性生成——流式处理
    }
}

// 消费方——也是协程:
Task process() {
    auto packets = read_packets(sock);
    while (auto pkt = co_await packets.next()) {
        process(pkt);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 9. 常见陷阱与反模式

# 9.1 coroutine_handle::destroy 忘记调用——内存泄露

Task t = async_work();
// ... t 离开作用域 → ~Task()
// 如果 ~Task 不调 handle_.destroy() → 协程帧泄露
// 这是案例 1.2 的一个变种

// 正确——RAII 包裹
~Task() { if (handle_) handle_.destroy(); }
1
2
3
4
5
6
7

# 9.2 promise_type 中取引用的悬挂——get_return_object 返回引用指向已析构的帧

struct promise_type {
    Task get_return_object() {
        // ❌ 返回 Task{*this, handle} → this 是 promise_type 的引用
        //    如果 final_suspend = suspend_never → 协程结束 → 帧析构
        //    → Task 里存的是悬垂引用
        return Task{*this, coroutine_handle<promise_type>::from_promise(*this)};
    }
};
1
2
3
4
5
6
7
8

正确:在 Task 中存储 coroutine_handle(8 字节的指针)——需要 promise 时通过 handle.promise() 动态获取。

# 9.3 在 final_suspend 中 resume 自己——无限递归

struct promise_type {
    std::suspend_always final_suspend() noexcept {
        // ❌ 不要在这里 resume 自己
        return {};  // suspend_always 是安全的——等调用方 destroy
    }
};
1
2
3
4
5
6

# 9.4 co_await 一个临时对象——await_suspend 返回时临时已析构

// ❌ awaiter 临时对象——在 co_await 表达式结束时析构
co_await IoAwaiter{sock};  // IoAwaiter 在这行结束时析构
// 但如果 await_suspend 中存了 coroutine_handle——恢复时的 await_resume 访问的是已析构的对象!

// ✅ awaiter 通过值传到帧上——或者保证生命周期跨越暂停
1
2
3
4
5

# 9.5 生成器的迭代器失效——在迭代期间修改协程帧

auto gen = fibonacci();
auto it = gen.begin();
auto val = *it;   // 读取第一个值
// 如果不 ++it——generator 的帧停留在第一个 co_yield 之后

gen = fibonacci(); // ❌ 旧的协程帧被 destroy——it 持有的 handle 悬空
auto val2 = *it;   // UB!
1
2
3
4
5
6
7

# 10. 综合案例串讲

# 10.1 案例真相揭晓

# 疑问 答案
① 三个关键字的语义? 第 4/6 章:co_await → awaiter 暂停恢复;co_yield → 暂停+传值;co_return → 结束
② promise_type 的接口时机? 第 3.1:编译器在协程帧构造→协程体执行→返回值→异常这条线上插入调用
③ awaiter 三函数顺序? 第 4.2:await_ready → await_suspend(暂停)→ await_resume(恢复后)
④ coroutine_handle 是怎么指针? 第 5.1:只是 void* → 8 字节——编译器知道如何从地址找回帧结构
⑤ 局部变量去哪了? 第 7 章:全搬进协程帧——堆上。暂停时帧不消失
⑥ 生成器的 compiler 魔法? 第 6.3:co_yield → promise.yield_value + 暂停——迭代器 resume + 读 current_val
⑦ 协程 vs future/thread? 第 8.2:协程帧 ~200B vs 线程栈 ~1MB——适合百万级连接
⑧ final_suspend 的选择? 第 3.3:suspend_always → 由调用方 destroy ——RAII 兼容

案例①修复——忘记 co_yield:确保协程体中有 co_yield / co_await / co_return 之一。使用静态分析工具检查 generator<T> 函数的返回体是否包含协程关键字。

案例②修复——promise_type 提前析构:

~Task() {
    if (handle_ && !handle_.done()) {
        handle_.destroy();       // ✅ 只要协程没结束——手动析构帧
    }
}
// 并确保 promise_type 中:
std::suspend_always final_suspend() noexcept { return {}; }
// → 协程结束时保持暂停——由调用方通过 destroy 安全析构帧
1
2
3
4
5
6
7
8

# 10.2 一次 co_await 的完整生命周期——从暂停到恢复

========================================
场景:协程通过 co_await 异步读 socket
========================================

① 协程帧已分配——在堆上(~240B)
② 协程执行到 co_await IoAwaiter{sock}
   → 编译器展开为:
     auto&amp;&amp; awaiter = IoAwaiter{sock};
     if (!awaiter.await_ready()) {          ← recv(MSG_DONTWAIT) = EAGAIN
         // 没有数据——需要暂停

         ③ awaiter.await_suspend(my_handle)
            → epoll_ctl(ADD, sock, data.ptr = my_handle.address())
            → my_handle 被保存在 epoll 的就绪列表中

         // === 协程现在暂停了 ===
         // 控制权返回到调度器
     }

④ 调度器运行事件循环:
     epoll_wait → sock 可读 → data.ptr = my_handle.address()
     → coroutine_handle&lt;>::from_address(data.ptr).resume()

⑤ resume → 恢复协程帧
   → resume_point = after_suspend
   → 进入 awaiter.await_resume()
     → recv(sock) 读数据

⑥ 协程继续执行——和同步代码完全一样的外观
========================================
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
29
30

# 10.3 设计哲学回扣

哲学 1:协程把「暂停后恢复」从操作系统级抽象降维到函数调用级

线程的暂停(阻塞)需要内核参与——保存整个线程上下文、调度出去、等 IO 就绪后再调度回来。协程的暂停不需要内核——只是函数 return(到调度器),下次 resume 从暂停点继续。这层抽象降维的意义:把「需要内核的上下文切换」(~3μs)变成「函数调用」(~5ns)——600× 的更轻量暂停。

哲学 2:栈less 不是缺陷——是把「什么需要跨暂停点保存」交给编译器分析

有栈协程的问题是:独立栈需要固定 1-8MB——不管协程只用了几 KB。栈less 协程让编译器精确分析需要保存的变量——只分配刚好够用的帧。这也是 C++「零开销抽象」哲学的延伸——不为不需要的存储付代价。 对于大量协程的场景(百万连接),每个协程省下 1MB 栈 = 省下 1TB 内存。

哲学 3:promise_type / awaiter / handle 的三角分离——编译时协变、运行时协作

三个概念各司其职:promise_type 控制协程的值语义(产什么、返什么、异常怎么传)、awaiter 控制暂停语义(何时停、何时恢复、恢复后取什么值)、handle 提供物理控制(resume / destroy / done)。这个三角把协程的三个维度——数据流、控制流、生命周期——在类型层彻底分离。 这是 C++ 设计语言的典型手法——用类型系统把正交关注点独立编码。

哲学 4:co_await 是显式的暂停点——不是隐式的 yield

C++ 的协程要求每个暂停点都显式标记(co_await / co_yield)。这和 Go 的 goroutine(隐式 yield)正好相反。显式的暂停点让你精确控制「哪里可能发生上下文切换」——这对多线程安全和性能分析至关重要。 这也是 C++ 的设计偏好:让代价在代码中可见。

哲学 5:对称转移是从协程到协程的直接跳转——消灭中间人

await_suspend 返回 coroutine_handle 实现对称转移——一个协程暂停时直接跳转到另一个协程——不经过调度器的中间栈帧。这和对称协程(如 Lua 的 coroutine.transfer)一致——把「调用链」压到最短。在 C++ 中这个优化利用了编译器对协程帧布局的精确了解——从帧 A 跳到帧 B 只是改了指令寄存器——不需要额外的栈操作。

# 10.4 速查表合集

协程三角速查:

组件 职责 关键操作 sizeof
promise_type 值/异常/生命周期控制 get_return_object, yield_value, initial_suspend, final_suspend ~40B (内嵌在帧中)
awaiter 暂停/恢复开关 await_ready, await_suspend, await_resume 取决于实现
coroutine_handle 帧的物理指针 resume, destroy, done, from_promise 8B (void*)

三个关键字编译器变换:

关键字 编译器展开 协程状态变化
co_await expr awaiter.await_ready() → await_suspend → await_resume 暂停→恢复
co_yield val co_await promise.yield_value(val) 暂停(值传给迭代器)
co_return promise.return_void() → final_suspend → 结束

suspend_always vs suspend_never:

策略 initial_suspend final_suspend
Generator suspend_always(惰性启动) suspend_always(由调用方 destroy)
Task(异步) suspend_never(立即启动) suspend_always(RAII 兼容)

promise_type 接口与协程生命期:

阶段 promise_type 接口 调用者
帧创建后 构造函数 编译器
调用方取结果 get_return_object() 编译器
第一行前 initial_suspend() 编译器(co_await)
co_yield 时 yield_value(val) 编译器(co_await)
co_return 时 return_void() / return_value(val) 编译器
异常时 unhandled_exception() 编译器
最后一刻 final_suspend() 编译器(co_await)

本篇小结:C++20 协程把「可暂停的函数」变成了一等公民。协程三角(promise_type / awaiter / coroutine_handle)把控制流、数据流和物理帧管理在类型层分离。栈less设计让每个协程只占 ~200B——比线程的 1MB 栈轻量 5000 倍。co_await 是显式的暂停点——把同步风格的代码自动展开为异步状态机。生成器、异步 IO、流式处理——三个场景覆盖了协程的核心价值:用同步代码写异步逻辑。

下一篇:六卷并发全部完成。下一篇进入卷七「编译链接与 ABI」——48.编译流程全景,从预处理到可执行文件的完整旅程。

上次更新: 2026/06/10, 11:13:41
无锁数据结构设计
翻译单元与预处理

← 无锁数据结构设计 翻译单元与预处理→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式