Effect 深度课effect@4.1.4 · LTS
并发与运行时底层

L19 ★ Scheduler:协作让出与公平性

⏱ 40 min难度 5/5前置:L18
本课目标
  • 解释 shouldYield 的触发条件
  • 知道如何调节操作预算与执行模式
前置课时尚未完成:L18

可以自行跳转学习,但按顺序推进会更省力;学情分析会把这类缺口归入「前置缺口」。

单线程上如何不饿死别人

Fiber 是协作式的,不是抢占式:一个 CPU 密集的 fiber 若不让出,整个事件循环都会被它占住。MixedScheduler 用「操作预算」解决:每个 fiber 记录 currentOpCount,达到 maxOpsBeforeYield 就插入 yieldNow,把续体排进调度队列。

Scheduler 接口与默认实现(示意)
← 代码可左右滑动 →
interface Scheduler {
  readonly executionMode: "sync" | "async"
  makeDispatcher(): SchedulerDispatcher
  shouldYield(fiber: Fiber): boolean
}

class MixedScheduler implements Scheduler {
  shouldYield(fiber) {
    return fiber.currentOpCount >= fiber.maxOpsBeforeYield
  }
  makeDispatcher() { return new MixedSchedulerDispatcher(setImmediate) }
}

// 默认值 2048 次操作;两个可调的 Context.Reference:
// MaxOpsBeforeYield / PreventSchedulerYield
💡 SchedulerDispatcher.scheduleTask(task, priority):数值小的优先级先跑,同优先级 FIFO;flush() 同步排空队列,测试里可得到确定性执行。

调度链路

  • fiber 执行若干条指令 → currentOpCount 增长。
  • shouldYield 命中 → 插入 yieldNow 指令,把续体交给 dispatcher。
  • dispatcher 按优先级入队,异步模式下经 setImmediate(无 setImmediate 时退回 setTimeout 0)回到宿主事件循环。
  • 下一轮 drain 取出任务,恢复 fiber 的 continuation 栈继续执行。
⚠️ sync 模式的代价

MixedScheduler("sync") 不向事件循环让出,CPU 密集任务会拖慢同进程其他请求。官方特别提醒 Cloudflare Workers 上 request-owned timer 可能被丢弃,需要整体使用 sync 模式并配合 HttpRouter.toWebHandler 的配方,但它并不修复 Effect.sleep 这类请求绑定的异步上下文。

交互演练
关键术语
协作式调度
fiber 在明确的让出点交出执行权,而非被抢占
MixedScheduler
默认调度器,支持 sync / async 模式与优先级队列
常见误区
× 把 Effect 的并发理解成多线程并行——JS 宿主仍是单线程。
× 为了「快」把 PreventSchedulerYield 全局打开,结果是长任务饿死 I/O。
随堂测验(即时反馈,答题计入掌握度)
Q1默认操作预算是多少,超了会怎样?
Q2调度队列的优先级规则是?