在开发中,我们经常写出这样的代码:
kotlin
mainScope.launch {
log("start")
delay(1000)
log("end")
}
这段代码看起来非常简单,但它隐藏了一个非常经典的问题:
delay 这 1 秒到底发生在哪个线程? 主线程前后都在执行,那中间谁在"等时间"?
结论
delay 从来不占用线程等待,它是一次"挂起 + 时间注册 + 调度恢复 + 状态机推进"的过程。
中间没有任何业务线程在 sleep,也没有线程在阻塞计时。
可以先运行我的 Demo:
👉 Delay 实现原理(自制 EventLoop) (非 Android 环境)
🧩 一、delay 的四阶段模型
为了彻底理解 delay,我们将其拆解为底层运作的四个连续阶段:
🧩 第一阶段:Suspend(挂起)
当代码执行到 delay(1000) 时,协程会立即触发挂起:
1. 保存 Continuation
后续的业务代码(如 log("end"))会被作为挂起点的剩余逻辑,封装成一个 Continuation(即状态机对象)。
2. 线程立即释放
当前线程(如 UI 主线程)发生的是:
- ❌ 没有
Thread.sleep() - ❌ 没有死循环阻塞
- ❌ 没有 CPU 空转
当前协程直接 "保存状态并释放当前线程的执行权"。如果是 Android 主线程,它会立刻回到底层的消息循环去处理其他 UI 绘制或点击事件,绝对不会卡顿。
🧩 第二阶段:Schedule(时间注册)
既然线程释放了,总得有人负责"记住这 1 秒"。这一阶段,协程会将刚刚封装好的恢复任务注册到对应环境的时间队列中:
🌟 情况 1:Android 环境(Dispatchers.Main)
协程最终会把任务交给 Android 的底层消息机制:
kotlin
handler.sendMessageDelayed(msg, 1000)
- 谁在等? 并不是协程在等,而是 Android 系统的
MessageQueue配合底层的nativePollOnce()(epoll 机制) 在管理时间。
🌟 情况 2:纯 JVM / 非 Android 环境
如果没有 Android Looper,delay 会 Fallback 降级到协程全局的:
- DefaultExecutor :一个全局的延迟守护线程。它通过
LockSupport.parkNanos()配合内部的延迟任务队列(时间轮或最小堆结构)来统一管理延迟。
本质 :无论是哪种环境,都是将任务托管给了底层的调度系统/守护线程推进时间。
🧩 第三阶段:Dispatch(调度恢复)
当 1000ms 时间到了之后,底层的计时器(Looper 或 DefaultExecutor)会触发回调。但注意:此时并不是直接盲目地去跑 log("end")!
任务会先经过:
kotlin
Dispatcher.dispatch()
- 为什么要这一步? 因为计时器触发时所在的线程,未必是协程原本要求的线程(例如
DefaultExecutor是一个独立的守护线程)。 - 核心职责 :
Dispatcher负责重新检查协程的上下文,并决定"在哪个线程去恢复这个协程"。它会将恢复任务重新post(投递)回目标线程的任务队列中(例如重新投递回 Android 的主线程队列)。
🧩 第四阶段:Resume(恢复状态机)
目标线程消费到这个调度任务后,协程框架重新接管控制权。
1. 恢复 Continuation
调用 continuation.resume(Unit),重新唤醒先前保存的协程状态机。
2. 执行剩余代码
协程从挂起点向下推进,最终执行:
kotlin
log("end")
整个生命周期至此完美闭环。
二、用一个自制 EventLoop 看清 delay 本质
为了彻底验证并看清上述四个阶段,我写了这套 Demo 来完美还原一个"协程 delay 调度系统"。
1. Looper(事件循环)
kotlin
Looper.prepare()
Looper.loop()
- 本质 :一个
while(true)的消息循环,用于模拟线程不阻塞时的任务消费。
2. Handler(任务投递)
kotlin
fun sendMessageDelayed(msg: Message, delayMillis: Long)
- 本质:负责把任务放入队列,并指定未来的执行时间。
3. Message(任务载体)
kotlin
class Message {
var whenTime: Long = 0L
var callback: Runnable? = null
var target: Handler? = null
}
- 本质:一个"未来要执行的动作包装"。
4. CoroutineDispatcher(协程桥接)
kotlin
class HandlerDispatcher(
private val handler: Handler
) : CoroutineDispatcher(), Delay
三、delay 的真实实现(核心拦截)
在我的自制调度器中,delay 的核心对接点在于实现 scheduleResumeAfterDelay 接口:
kotlin
override fun scheduleResumeAfterDelay(
timeMillis: Long,
continuation: CancellableContinuation<Unit>
) {
// 【对应 Dispatch & Resume】把"恢复协程"封装成 Runnable 任务
val block = Runnable {
with(continuation) {
// 官方的标准实现会经过 dispatcher.dispatch 重新分发线程
// 这里在 Looper 线程直接恢复
resumeUndispatched(Unit)
}
}
// 【对应 Schedule】封装成 Message,丢进延迟队列
val msg = Message().apply {
callback = block
}
handler.sendMessageDelayed(msg, timeMillis)
}
🔥 四、完整 delay 执行链路图
text
launch (启动协程)
↓
delay(1000)
↓
【1. Suspend】挂起协程(保存 Continuation 状态,释放当前线程)
↓
【2. Schedule】调用 scheduleResumeAfterDelay → 封装 Message(callback = resume) 投入队列
↓
【底层的等待】MessageQueue / DefaultExecutor 在不阻塞业务线程的前提下推进时间
↓
【3. Dispatch】时间到!Looper 捞出消息,通过 Dispatcher 分发,决定在哪个线程恢复
↓
【4. Resume】调用 continuation.resume() 恢复状态机
↓
继续执行 "end"
五、结论
- **
delay的本质是:状态机挂起(Suspend) → 时间注册(Schedule) → 线程分发(Dispatch) → 回调恢复(Resume)。 - 整个挂起期间,没有线程 sleep、没有线程阻塞、没有 CPU 空转,有的只是调度系统在极其高效地推进时间。
🚀 六、这套demo的价值
这套代码虽然是一个 Demo,但它它用最精简的代码表达了:EventLoop、MessageQueue、Dispatcher、Delay 以及 Continuation 之间的闭环协作。