协程原理
Kotlin协程的本质是通过状态机管理挂起点,由编译器进行CPS变换实现的轻量级并发抽象。其核心原理和状态推进机制如下:
核心原理
1. 挂起函数
- 用suspend修饰的函数
- 编译器会将其编译为状态机代码(而非阻塞线程),支持在任意位置挂起/恢复
2. 续体
- 类似回调的接口
Continuation<T>
,其关键方法是resumeWith(result)
- 协程的每一步执行都依附于一个续体对象,存储当前执行状态和上下文
3. 状态机转换
- 编译器将挂起函数拆解成一个状态机(通过
label
标记状态) - 每个挂起点对应一个状态迁移
状态推进流程
以下代码展示状态机的运作:
suspend fun fetchData(): String {
val data1 = fetchPart1() //挂起点1
val data2 = fetchPart2() //挂起点2
return data1 + data2
}
编译器转换后(伪代码)
class FetchDataStateMachine(
val completion: Continuation<String>,
var label: Int = 0
) : Continuation<Unit> {
var data1: String? = null
var data2: String? = null
override fun resumeWith(result: Result<Any?>) {
when(label) {
0 -> {
label = 1
fetchPart1(this)
}
1 -> {
data1 = result.getOrThrow() as String
label = 2
fetchPart2(this)
}
2 -> {
data2 = result.getOrThrow() as String
completion.resumeWith(data1 + data2) //返回最终结果
}
}
}
}
关键机制
1. 挂起不阻塞线程:
- 协程挂起时,底层线程立即释放(例如返回到线程池),避免资源浪费
- 异步操作完成后,任务被派发到合适的线程继续执行(通过
Dispatcher
)
2.续体传递风格
- 挂起函数被编译为接受额外
Continuation
参数的函数 - 例如
suspend fun foo()
→fun foo(continuation: Continuation)
3. 协程上下文(CoroutineContext)
- 通过
CoroutineContext
传递调度器、异常处理器等。 - 状态机中通过
Continuation.context
获取当前上下文
4. 结构化并发
- 协程树通过父-子关系管理生命周期
- 父协程取消时,自动取消所有子协程
状态推进
在FetchDataStateMachine
的resumeWith
中并没有循环,label的状态是如何推进的呢?实际上状态推进是通过递归链式调用与间接跳转实现的。
1. 单次触发模型
- 每次resumeWith被调用时只处理当前状态
- 通过更新label值标记下一步状态
- 不立即处理后续状态,而是等待下一次恢复
label = 2 //只标记下一步状态,不立即执行
fetchPart2(this) //触发异步操作(挂起),this就是FetchDataStateMachine,其是Continuation,可通过this调用resumeWith
2. 链式递归唤醒
- 每个异步操作完成时,都会重新调用resumeWith
- 每调用一次,就会处理当前状态并设置下一次状态
resumeWith(结果) → 处理当前状态
↑ ↓
异步完成 设置下一状态
↑
恢复执行
3. 状态变量持久化
- 状态机对象在挂起期间持续存在(堆内存)
- 成员变量(data1, label)保存中间状态
- 每次恢复时从正确状态继续执行
4. 编译器优化技巧
- 尾递归优化:编译器会将状态处理转为循环
- 状态折叠 :合并可优化状态减少跳转次数
- 内联状态:简单状态机转为switch跳转表
对挂起的理解
协程挂机:在挂起点暂停当前的同步代码,转而去执行消息队列的runnable;这样就是我对挂起的理解,也就是让出线程