协程 ≠ 异步框架(终章)

Selector 是轮子,协程是车身——同一根棍子,天差地别的写法

协程 vs 异步 回调地狱 Promise 九章串联 终极总结

目录导航(点击跳转)

一、先把异步框架拉出来对比

1三种写法,同一个功能
// ========== 异步框架(回调版) ==========
function loadPage(userId) {
    fetchUser(userId, function(user) {               // ① 回调
        fetchOrders(user.id, function(orders) {       // ② 回调套回调
            fetchRecommendations(user.id, function(recs) { // ③ 再套
                renderPage(user, orders, recs);        // ④ 真正的活
            });
        });
    });
}  // 回调地狱。每个异步操作都要包一个函数,逻辑被撕碎。
2Promise 版:好一点,但仍然割裂
// ========== 异步框架(Promise 版) ==========
function loadPage(userId) {
    return fetchUser(userId)
        .then(user => fetchOrders(user.id))           // 链式,好一点
        .then(orders => {
            return fetchRecommendations(userId)
                .then(recs => renderPage(user, orders, recs));
        });  // 还是得拆成 then,user 变量传递麻烦
}
3协程版:和同步代码一模一样
// ========== 协程版 ==========
suspend fun loadPage(userId: String) {
    val user   = fetchUser(userId)              // ① 看起来阻塞
    val orders = fetchOrders(user.id)           // ② 同上
    val recs   = fetchRecommendations(userId)   // ③ 同上
    renderPage(user, orders, recs)              // ④ 真正的活
}  // 和同步代码一模一样。顺序写,顺序读。

二、你写的代码 vs 实际跑的东西

1一张图看清
异步框架(回调/Promise)
你写的:①→②→③ (顺序写)
实际跑的:①→[出去]→②→[出去]→③ (被撕成碎片)
协程
你写的:①; ②; ③ (顺序写)
实际跑的:①→[挂起]→②→[挂起]→③ (编译器帮你拼)

协程让你用顺序的脑子写异步的活。你写的是故事,编译器帮你拍成分镜。

三、整个系列串成一条线

1九章知识地图
第 1 章  协程是可暂停再继续的函数  ← 基本概念
第 2 章  挂起不阻塞线程,线程可以去跑别的  ← 和 sleep 的本质区别
第 3 章  编译器把函数切成状态机,续体存堆上  ← 挂起/恢复的内部机制
第 4 章  协程的栈在堆上,按需增长  ← 为什么内存省
第 5 章  作用域:父子连锁,结构化并发  ← 生命周期管理
第 6 章  调度器:决定协程在哪个线程上跑  ← 协程不绑定线程
第 7 章  Runtime 全貌:一台引擎撑百万并发  ← 棍子 + 续体 + 线程池
第 8 章  Go / Java / Kotlin 各自实现  ← 换汤不换药
第 9 章  协程 vs 异步框架:同一根棍子  ← 终极对比

四、终极一句话

1从代码到引擎的完整链路
顺序阻塞代码
编译器切状态机
堆上续体对象
挂起点挂起
线程跑别的协程
epoll 监听 fd
socket 有数据
→ 唤醒续体
下一个 case 继续
=
你看是阻塞
底下非阻塞+状态机+epoll
2你想写的 vs 实际发生的

你想写的(简单但不可能)

void loadData() {
    User u = fetch();  // 卡住
    show(u);
}
// OS 线程: 卡死,浪费

实际发生的(复杂但自动)

loadData 状态机:
  case 0: 发起 fetch → 挂起
  case 1: 拿到 u → show(u)

底层: epoll 监听 fd
OS 线程: 跑千千万万个协程

五、整趟旅程的知识点总览

1从阻塞到百万并发:一条完整路径
1
阻塞→ 线程卡死,一个连接一个线程
2
非阻塞→ 线程不卡,来回轮询,"棍子转得够快就能拦截住"
3
Selector→ OS 替你转棍子,有事件才通知,CPU 不空转
4
协程→ 写阻塞式代码,编译器切成状态机,底层还是 Selector
5
状态机→ 函数被切成 case,continuation 对象在堆上,局部变量变字段
6
挂起/恢复→ 线程和协程解绑,挂起时线程跑别的,恢复时可能是另一个线程
7
作用域→ 结构化并发,父等子、子死全家死、爹死全家死
8
调度器→ Dispatcher 决定线程池,Main/IO/Default/Unconfined
9
Runtime→ while True + epoll_wait + 就绪队列 + 定时器队列
10
语言差异→ Go 全自动,Java 兼容老 API,Kotlin 显式染色

六、核心思想总结

协程 = 编译器替你写回调

九章完结 · 终极一句话

你写的顺序阻塞代码 → 编译器切成状态机 → 堆上的续体对象 → 每次 I/O 挂起点,协程 suspended,线程跑别的协程 → epoll(那根棍子)在所有连接之间转,谁有数据叫醒谁 → 续体恢复,从下一个 case 继续 → 你看是阻塞,底下是非阻塞 + 状态机 + epoll。

协程 = 编译器替你写回调。