Kotlin 协程底层原理(Continuation)详解
很多人会用协程:
kotlin
launch { }
async { }
withContext { }
但真正理解协程底层后,你会发现:
Kotlin 协程本质上并不是"魔法异步"。
它本质是:
Continuation(续体) + 状态机
这才是协程真正核心。
一、先理解:为什么需要 Continuation?
先看普通函数。
普通函数执行过程
kotlin
fun test() {
println("1")
println("2")
println("3")
}
执行流程:
1 -> 2 -> 3
函数执行完:
- 栈帧销毁
- 无法恢复
这叫:一次性执行。
但协程不同。
二、协程为什么能"暂停后恢复"?
kotlin
suspend fun load() {
println("开始")
delay(3000)
println("结束")
}
问题来了:
delay 后,协程暂停了。3秒后,它为什么还能继续从 println("结束") 开始执行?
普通函数做不到。
所以 Kotlin 编译器做了一件事:
把 suspend 函数改造成**"状态机"**
而 Continuation,就是保存状态的核心对象。
三、Continuation 是什么?
官方定义:
"协程执行到哪了"的记录器。
或者更简单:
Continuation = 协程存档点
它会记录:
- 当前执行位置
- 局部变量
- 下一步执行逻辑
- 协程上下文
四、最简单理解
你玩游戏:
- 打到一半存档
- 退出游戏
- 下次继续
Continuation,就是这个"存档"。
五、suspend 本质是什么?
先看:
kotlin
suspend fun request(): String
很多人以为 suspend 是关键字魔法。
其实编译后 ,suspend 函数会变成:
kotlin
fun request(
continuation: Continuation<String>
): Any
重点来了:
所有 suspend 函数,都会多一个 Continuation 参数
这是协程底层最大核心。
六、编译器偷偷干了什么?
你写:
kotlin
suspend fun test() {
delay(1000)
println("完成")
}
编译器会生成类似:
kotlin
class TestContinuation : Continuation<Unit> {
var label = 0
override fun resumeWith(result: Result<Unit>) {
when(label) {
0 -> {
label = 1
delay(1000, this)
}
1 -> {
println("完成")
}
}
}
}
这就是:
状态机(State Machine)
七、label 是什么?
kotlin
var label = 0
它表示:当前协程执行到了哪个阶段
示例
kotlin
suspend fun demo() {
println("A")
delay(1000)
println("B")
delay(1000)
println("C")
}
编译后类似:
| label | 位置 |
|---|---|
| 0 | 开始 |
| 1 | 第一个delay后 |
| 2 | 第二个delay后 |
所以:
协程恢复时,只需要看 label,就知道该从哪里继续。
八、delay 为什么不会阻塞线程?
这是经典面试题。
Thread.sleep
kotlin
Thread.sleep(3000)
线程:彻底卡死
delay
kotlin
delay(3000)
发生了什么?
第一步:协程保存现场
- Continuation 存档
- 包括:label、局部变量、执行位置
第二步:当前线程被释放
- 线程去执行别的任务
第三步:时间到了
- 恢复 Continuation:
continuation.resume() - 协程继续执行
所以:delay 挂起的是协程,不是线程。
九、resume 是什么?
Continuation 最核心方法:
kotlin
resumeWith()
作用: 恢复协程执行
举例:
- 协程暂停在
delay - 时间到 →
resume() - 协程继续
十、真正的协程恢复流程
代码
kotlin
launch {
delay(1000)
println("Hello")
}
底层流程
1. launch 创建协程
→ 生成:Continuation对象
2. 执行到 delay
→ 发现:需要挂起
3. 保存状态
→ 保存:label、局部变量、上下文
4. 当前线程释放
→ 线程不阻塞
5. 1秒后
→ 定时器触发:continuation.resume()
6. 状态机恢复
→ 从:label = 1 继续执行
十一、协程为什么这么轻量?
| 线程 | 协程 |
|---|---|
| OS内核调度 | 用户态对象 |
| 成本高 | 只是一堆 Continuation + 状态机对象 |
所以:
一个线程可以跑几十万个协程
十二、挂起点(Suspension Point)
只有 suspend 函数才能挂起。
例如:
delay()withContext()await()
这些都叫:挂起点
十三、为什么 suspend 只能在协程里调用?
因为:
suspend函数需要Continuation
普通函数没有。
所以:
kotlin
fun main() {
delay(1000) // 会报错
}
十四、withContext 为什么能切线程?
kotlin
withContext(Dispatchers.IO)
本质:
- 修改 Continuation 的 Dispatcher
- 恢复协程时,调度器决定在哪个线程继续执行
十五、CoroutineContext
协程上下文,内部保存:
- Dispatcher
- Job
- CoroutineName
- ExceptionHandler
本质: 就是 Map 结构
十六、Job 底层原理
每个协程都有 Job,Job 维护:
- 父子协程关系
cancel()本质:修改协程状态,然后挂起点检测取消
十七、为什么协程取消不是立刻停止?
因为:协程是协作式取消,不是线程强杀。
例如:
kotlin
while(true) { } // 不会停,因为没有检查取消状态
正确:
kotlin
while(isActive) { }
十八、suspend 不等于异步
这是最容易误解的。
| 错误理解 | 正确理解 |
|---|---|
suspend = 开新线程 |
suspend 只是支持挂起与恢复,是否异步看你是否 launch / async |
十九、真正的协程核心结构
协程底层可以简化成:
Coroutine
↓
Continuation
↓
StateMachine
↓
Dispatcher
二十、协程 vs 回调
回调地狱
kotlin
request {
request2 {
request3 {
// ...
}
}
}
协程
kotlin
val a = request()
val b = request2()
val c = request3()
为什么能这样? 因为 Continuation 替你保存了执行现场。
二十一、Retrofit 为什么支持 suspend?
Retrofit 遇到 suspend fun getUser():
本质: 它拿到了 Continuation
网络返回后,调用 continuation.resume(data),协程恢复。
二十二、最重要的一张图
普通函数
执行 -> 结束 -> 消失
协程
执行
↓
挂起
↓
保存Continuation
↓
线程释放
↓
恢复resume
↓
继续执行
二十三、协程源码里最重要的类
真正核心:
- Continuation(基类)
- ContinuationImpl
- BaseContinuationImpl
- DispatchedContinuation
二十四、源码中最经典的方法
kotlin
resumeWith()
协程恢复入口。
二十五、面试高频题(非常重要)
1. suspend 本质是什么?
本质:编译器 CPS 转换
即 Continuation Passing Style
编译器自动添加 Continuation 参数。
2. 协程为什么不卡线程?
因为:挂起的是协程,不是线程
3. 协程为什么轻量?
因为:不走系统线程调度,本质只是对象和状态机
4. delay 为什么能恢复?
因为:continuation.resume()
5. 协程如何记住执行位置?
通过:label 状态机
二十六、真正理解协程的一句话
Kotlin 协程本质:
编译器把 suspend 函数转换成状态机
然后 Continuation 保存协程执行现场
最终实现:可暂停、可恢复、非阻塞