Runtime 调度全貌

一根棍子(epoll)+ 一堆续体 + 线程池 + 定时器 = 一台完整的协程引擎

Runtime 引擎 epoll_wait 调度器队列 定时器队列 线程池

目录导航(点击跳转)

一、先把所有零件摊开

1六大零件
#零件作用
epoll(棍子)OS 帮你监听所有网络连接,有事件才通知
续体(状态机)每个协程是一小段可暂停的函数,活在堆上
定时器队列按时间排序的协程列表,"delay 到了叫醒我"
线程池几个 OS 线程,负责实际执行协程代码
Dispatcher决定哪个协程交给哪个线程池
作用域父子关系,取消传播

二、引擎全景图

1完整引擎架构
新协程launch { ... } / 恢复的协程──→
调度器队列(就绪协程列表)
线程池[T1][T2][T3]... ← 工人 ← 执行协程代码
遇到 delay(1000)→ 定时器队列(按到期时间排序)
遇到 read(socket)epoll(监听所有 fd)
epoll_wait(timeout=最近定时器剩余)← 等三件事:①定时器到期 ②socket有数据 ③新协程launch
唤醒的协程 塞回调度器队列──→ 循环

三、一步一步跟着走一遍

1三个协程示例
// 协程A: 网络请求
launch { val data = httpGet("https://api.example.com/user"); cache(data) }

// 协程B: 定时等待
launch { delay(2000) }

// 协程C: CPU 计算
launch { heavyComputation() }
2Step 1-4:执行流程
调度队列 线程 定时器/epoll
Step 1:全部启动→ 调度队列: [A, B, C],线程取出 A
AhttpGet → 发起 TCP 连接 → fd 注册 epoll → 挂起
Step 2:A 挂起→ 调度队列: [B, C],epoll: {fd_A→A},线程取出 B
Bdelay(2000) → 续体B 进定时器队列 → 挂起
Step 3:B 挂起→ 调度队列: [C],定时器: [(B, +2000ms)],线程取出 C
C纯 CPU 计算 → 没挂起 → 一路跑到底 → 完成
Step 4:队列空了→ 线程调 epoll_wait(timeout=2000ms),睡觉但不是傻睡
3Step 5-7:唤醒与收工
调度队列 线程 epoll/定时器
Step 5 (1000ms后):HTTP 响应到达 fd_A → epoll 检测到事件 → 唤醒线程
A续体A 塞回就绪队列 → 线程执行 → cache(data) → 完成
调度队列又空了 → epoll_wait(timeout=1000ms,B 还剩的)
Step 6 (又1000ms后):timeout 到期 → epoll_wait 返回空 → 检查定时器队列
B续体B 进就绪队列 → 线程执行 → println("2秒到了") → 完成
Step 7:全部收工。epoll_wait(timeout=∞) → 永久等新协程或被 eventfd 唤醒
4epoll_wait 同时等三件事
// 这就是那根棍子!线程在这等,不是轮询
epoll_wait(
    epoll_fd,                        // epoll 实例
    events,                          // 输出:哪些 fd 有事件了
    timeout = 到最近定时器的时间差      // ← 关键!不是永久等
);

// timeout 计算:
//   定时器队列最早到期:协程B = 10:00:02.000
//   现在:10:00:00.000
//   timeout = 2000ms

// 同时等三件事:
//   ① socket fd_A 有数据了 → 立刻醒
//   ② 等了 2000ms 还没数据 → 也醒了(定时器到期)
//   ③ 新协程被 launch → eventfd 通知 → 醒了

epoll_wait 的 timeout 是动态计算的——每次取值都看定时器队列里最早到期的那个还剩多久。网络事件到来也会提前返回,不是傻等到 timeout 结束。

四、完整时间线表格

17 步时间线
时间线程在干嘛epoll定时器调度队列
0ms跑协程A:httpGet → 挂起 → fd_A 注册 epoll{fd_A→A}[A,B,C]→[B,C]
0ms跑协程B:delay(2000) → 续体B 进定时器{fd_A→A}{(B,2000)}[C]
0ms跑协程C:CPU计算 → 完成{fd_A→A}{(B,2000)}[]
0-1000ms队列空,睡!epoll_wait(timeout=2000){fd_A→A}{(B,2000)}[]
1000msfd_A 有数据,被唤醒!续体A 进队列{fd_A→A}{(B,1000)}[A]
1000ms跑协程A(从 httpGet 后继续):cache(data) → 完成{fd_A→A}{(B,1000)}[]
1000-2000ms队列空,睡!epoll_wait(timeout=1000){fd_A→A}{(B,1000)}[]
2000mstimeout 到期,续体B 进队列{fd_A→A}[B]
2000ms跑协程B:println("2秒到了") → 完成{fd_A→A}[]
2000ms+全空,永久睡。epoll_wait(timeout=∞) 等新协程{fd_A→A}[]

整个过程中,只有1 个线程0 次轮询0 次 CPU 空转

五、这个引擎为什么高效?

1阻塞模型 vs 协程 Runtime
阻塞模型
线程1 ─┤ fd_A (卡死)
线程2 ─┤ fd_B (卡死)
线程3 ─┤ fd_C (卡死)
...
1000连接 = 1000线程 = 1GB栈
全部各自阻塞,大部分在睡觉
协程 Runtime
一个线程 ─┬─ 协程A (fd_A 挂起)
          ├─ 协程B (timer 挂起)
          ├─ 协程C (跑 CPU)
          ├─ 协程D (fd_X 挂起)
          ├─ 协程E (timer 挂起)
          └─ epoll_wait ← 全在这一个调用
所有连接 = 一个 epoll_fd

六、跑起来的伪代码(这才是精髓)

1Runtime 核心循环
# 协程 Runtime 的核心循环(伪代码)

ready_queue = []                    # 就绪协程
timer_queue = PriorityQueue()       # 定时器队列,按到期时间排序
epoll_fd = epoll_create()

while True:
    # 第 1 步:处理就绪协程
    while ready_queue:
        coroutine = ready_queue.pop(0)
        result = coroutine.resume()  # 执行协程,直到它挂起

        if result == SUSPENDED_ON_FD:
            # 协程在等 socket → 注册到 epoll
            epoll_ctl(epoll_fd, ADD, coroutine.fd, coroutine)

        elif result == SUSPENDED_ON_TIMER:
            # 协程在等定时器 → 塞进定时器队列
            timer_queue.push(coroutine, wakeup_time)

        elif result == COMPLETED:
            pass  # 结束了,啥也不用干

    # 第 2 步:没就绪协程了,调 epoll_wait 等
    next_timer = timer_queue.peek()  # 最近的定时器

    if next_timer is None:
        timeout = -1  # 没定时器 → 永久等
    else:
        timeout = max(0, next_timer.wakeup_time - now())

    events = epoll_wait(epoll_fd, timeout=timeout)

    # 第 3 步:醒来,三种可能被唤醒的原因

    # ① socket 有数据了
    for event in events:
        ready_queue.append(event.bound_coroutine)

    # ② 定时器到了
    while timer_queue and timer_queue.peek().wakeup_time <= now():
        ready_queue.append(timer_queue.pop())

    # ③ epoll_wait 被新 launch 的协程通过 eventfd 唤醒
    #    (eventfd 也是 epoll 注册的一个 fd)
    #    处理同 ①,循环回到第 1 步

    # 回到循环开头,继续处理就绪队列

七、核心思想总结

第七章核心要点

一句话总结第七章

一个协程引擎 = while True 循环:1.拿就绪协程→跑→挂起了注册到 epoll 或定时器队列;2.队列空了→epoll_wait(timeout=最近定时器剩余时间)→线程睡觉;3.醒了→把唤醒的协程塞回就绪队列→回到 1。整个引擎就一个循环,几个线程,一堆堆上的续体对象,一根棍子(epoll)+ 一个闹钟队列,撑起百万并发。