调度控制与启动模式
协程的执行并非总是“立即异步”的。通过配置启动模式和显式调用调度原语,开发者可以精准控制协程的执行时机与线程公平性。
启动模式: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 的双重作用
- 公平性:防止长耗时任务霸占 CPU。
- 响应取消:
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.Default 或 Dispatchers.IO 中,协程并不是按照严格的 FIFO(先进先出)执行的,但在单个线程内部,它们通常遵循队列顺序。
yield vs. delay(0)
yield(): 明确表示“我还没干完,但可以让别人先跑”。delay(0)/delay(1): 强制触发一次完整的挂起恢复流程,通常用于调试或处理某些特定的竞态条件,但开销比yield()更大。
核心准则总结
- 默认使用
DEFAULT:只有在需要优化初始化延迟或关键资源分配时才考虑UNDISPATCHED或ATOMIC。 - 长循环必须包含
yield()或isActive检查:这是编写“协程友好型”代码的基本要求。 - 理解 LAZY 的触发时机:
await()会触发 LAZY 协程,但如果不调用start()或join(),它可能永远不会执行。