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