Skip to content

调度控制与启动模式

源:CoroutineStart API

协程的执行并非总是“立即异步”的。通过配置启动模式和显式调用调度原语,开发者可以精准控制协程的执行时机与线程公平性。

启动模式:CoroutineStart

启动模式定义了协程从“创建”到“第一行代码执行”之间的行为。

模式立即分派?是否挂起?特性描述
DEFAULT立即进入调度器队列,等待分派执行
LAZY仅在手动调用 start()join() 后启动
ATOMIC首个挂起点前不可取消,确保关键初始化完成
UNDISPATCHED否 (同步)在当前线程立即同步执行,直到遇到第一个挂起点

深度解析:UNDISPATCHED 的黑客技巧

UNDISPATCHED 会跳过初始的分派(Dispatch)过程。它在当前线程立即执行,直到遇到第一个挂起点。

kotlin
// 1. 传统方式:涉及两次分派开销
launch(Dispatchers.IO) { 
    doSimpleCheck() // 已经在 IO 线程
    doHeavyIO() 
}

// 2. UNDISPATCHED:节省第一次分派
launch(Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) {
    doSimpleCheck() // 在当前线程 (如 Main) 立即执行
    delay(10)      // 挂起
    doHeavyIO()    // 恢复时切换到 IO 线程
}

协作式调度:yield

协程是基于协作的。如果一个协程执行的是纯计算任务且没有挂起点,它会一直占用线程,导致同调度器下的其他协程“饿死”。

让出执行权

yield() 函数的作用是将当前协程挂起,并立即放回调度队列的末尾,给其他协程执行的机会。

yield 的双重作用

  1. 公平性:防止长耗时任务霸占 CPU。
  2. 响应取消yield() 内部会检查当前 Job 是否已取消,如果是,则抛出 CancellationException

实战:防止线程饥饿

kotlin
launch(Dispatchers.Default) {
    for (i in 1..10_000_000) {
        processData(i)
        // 每 1000 次迭代让出一次执行权,保证系统整体响应性
        if (i % 1000 == 0) yield() 
    }
}

调度公平性 (Scheduling Fairness)

Dispatchers.DefaultDispatchers.IO 中,协程并不是按照严格的 FIFO(先进先出)执行的,但在单个线程内部,它们通常遵循队列顺序。

yield vs. delay(0)

  • yield(): 明确表示“我还没干完,但可以让别人先跑”。
  • delay(0) / delay(1): 强制触发一次完整的挂起恢复流程,通常用于调试或处理某些特定的竞态条件,但开销比 yield() 更大。

核心准则总结

  1. 默认使用 DEFAULT:只有在需要优化初始化延迟或关键资源分配时才考虑 UNDISPATCHEDATOMIC
  2. 长循环必须包含 yield()isActive 检查:这是编写“协程友好型”代码的基本要求。
  3. 理解 LAZY 的触发时机await() 会触发 LAZY 协程,但如果不调用 start()join(),它可能永远不会执行。