kotlin协程suspend背后的逻辑和状态机思想
一:前置知识
1:状态机是什么
状态机state machine是什么?它就是用来表示一个对象处于什么状态然后决定正确状态下做正确的事。
比如说:饮料机在没有扫码的时候处于0状态,就不能调用开门这个方法,而扫码之后,饮料机的状态改变,才可以开门,但是扫了码就不能再扫了,状态不同,做的事也不同,状态之间因为做了一些事情而不断转移。示例中状态在变化,行为也在变化。
fun main() { val stateMachine = StateMachine() repeat(3) { when (stateMachine.state){ 0 -> { println("状态0做事") stateMachine.state = 1 } 1 -> { println("状态1做事") stateMachine.state = 33 } else -> { println("其他状态做事") } } } } class StateMachine { var state = 0 } 控制台: 状态0做事 状态1做事 其他状态做事
2:回调函数是什么
回调函数就是一个命令。
fun function1(callback: () -> Unit) { do some thing... do some thing... callback() }
上面的callback就是一个回调,我们传”唱歌“进去那最后就会唱歌,传”跳舞“就会跳舞。不过大多数情况像okhttp中呢,一般会需要传一个类,类中有成功的回调也有失败的回调,我们要实现两个,我写的只是想表达这么一个概念。
3:回调函数+状态机
我们稍后会看到,suspend函数的背后,就是编译器compile把我们的suspend函数变成一个回调函数+状态机,但现在,我们先来看一个简化版本,循序渐进。
fun main() { myFunction(null) } interface Callback { 一个回调接口 fun callback() { } } fun myFunction(Callback: Callback?) { class MyFunctionStateMachine : Callback { 实现回调接口并加上状态 var state = 0 override fun callback() { myFunction(this)//调用本函数 } } val machine = if (Callback == null) MyFunctionStateMachine() else Callback as MyFunctionStateMachine when (machine.state) { 0 -> { println("状态0做事") machine.state = 1 machine.callback() } 1 -> { println("状态1做事") machine.state = 33 machine.callback() } else -> { println("其他状态做事") machine.state = 0 把状态又置于0,循环执行 machine.callback() } } }
我们定义了这样一个函数,这个函数接收一个回调作为参数,我们在类内部实现了这个回调接口,并加上状态,并在回调中调用了函数本身,并把更新过后的自己传了进去。在第一次执行时,因为是null,所以会创建,后续就不会创建了。在这个自己调用自己的过程之中,每一次调用自己,状态都不一样。
二:kotlin中的回调接口
public interface Continuation<in T> { .... public fun resumeWith(result: Result<T>) }
不过就是把类名和函数名换了而已。这个参数是什么呢?这个Result类可以当成是包含了:一个value, 一个boolean表示成功还是失败。所以我们知道了协程中有这样一个接口。
三:suspend函数变为字节码之后
1:函数签名和返回值的改变
对于suspend标记的函数,在转为Java字节码之后,会在函数签名中加一个参数:
Continuation $completion
比如:
suspend fun work1_1() { ... } 变为 public static final Object work1_1(@NotNull Continuation $completion) { ... }
suspend fun func(para1: Type1, para2: Type2) 变为 func(Type1 para1, Type2 para2, Continuation $completion)
另外,除了函数签名变化之外,函数的返回类型也会变为Any?(表现在Java字节码中则为Object),这是因为如果suspend函数里面调用了delay之类的函数导致suspended发生的话,函数会返回一个enum类型:COROUTINE_SUSPENDED
internal enum class CoroutineSingletons { COROUTINE_SUSPENDED, UNDECIDED, RESUMED }
所以用Any?作返回类型的话就比较合适。
2:函数体的改变
函数签名中接收的是父suspend函数,即调用当前suspend函数的suspend函数传下来的状态机。
比如suspend function1调用了suspend function2,那么function2签名中收到的就是function1中定义的状态机。
2.1:加入状态机类
每个suspend函数内都会有一个状态机类,比如
suspend fun work1(): String { println("1") work1_1() return "1" } suspend fun work1_1() { println("1_1") } 那么work1的字节码大概是:
public static final Object work1(Continuation parentContinuation) { class Work1Continuation(val parentContinuation: Continuation):Continuation{
val label = 0
val result = null
fun callback(result: Result){
this.result = result
work1(this)//自己调用自己
}
}
.......
}
2.2:加入是否已创建的判断
然后,如果是第一次调用的话,传下来的是父的continuation,因此会有一个判断,判断continuation是否是当前类的continuation,因为以后调用自己,就是自己的那个continuation了。
public static final Object work1(Continuation parentContinuation) { 内部continuation类: val continuation = parentContinuation as? Work1Continuation ?: Work1Continuation(parentContinuation) ....... }
如果是第一次进这个函数,那么
parentContinuation as? Work1Continuation == null
就会创建,如果不是第一次进入,前面我们看到,会把自己传入,所以可以成功转换。
2.3:加入状态判断
public static final Object work1(Continuation parentContinuation) { 内部continuation类: val continuation = parentContinuation as? Work1Continuation ?: Work1Continuation(parentContinuation) when(continuation的label){ 0 -> 第一次进入 println("1") continuation.label = 1 val result = work1_1() continuation.callback(result) 1-> 第二次进入 continuation.parentContinuation.callback("1") else -> 报错 } }
一个suspend函数里面有4个suspend函数的话,就会有5种状态,分别是
- 0 第一次进入
- 1 从第一个suspend中恢复
- 2 从第二个suspend中恢复
- 3 从第三个suspend中恢复
- 4 从第四个suspend中恢复,在这里的最后,调用了父的continuation的回调,因此父又调用自己本身,就回到上一层
2.4:总结
suspend函数func结构:
1: 这个函数的Continuation匿名类 2:检验是否是第一次进入,第一次进入就创建Continuation,并把上层传进来的Continuation包在本层的Continuation里 3:状态机 case 第一次进来 设置标志位为:第二次进来
调用第一个suspend函数
调用这个函数的continuation的callback
case 第二次进来 。。。。 case 最后一次进来 调用上层函数的Continuation的callback(本层函数的Continuation的result)
所以,协程框架为我们顺序执行的代码,转换成了回调的形式。父函数里面调用子函数,子函数假如挂起了,是一个delay,那么当delay时间过去之后,因为delay也是个挂起函数,它结束了,就调用父continuation的回调(我们的函数不是把自己的continuation传给delay了吗?),结果父函数的continuation调用父函数,再次进来的时候状态变了,就不会再走之前走过的代码了。
这样,我们通过label,就相当于保存了挂起点。
四:为什么协程知道从哪里恢复
接上面的话,我们调用了delay之后,协程被挂起,线程清空当前函数调用栈,转去做其他事情,那么协程是如何知道从哪里恢复执行的呢?其实上面已经说了,就是利用continuation调用自己的这个特性。
- 调用栈如何恢复?调用哪个函数,由continuation本身就可以由调用自己的这个特性来恢复
- 函数内的局部变量如何恢复?函数内的局部变量不是清空了么?其实也都变成continuation的字段了,我上面没说。
- 函数内执行到哪里如何恢复?由label变量来恢复
在这里,不得不感慨太妙了,通过子continuation引用父continuation
最上层Continuation(这一层的局部变量,label) <----被引用---- 第二层Continuation(这一层的局部变量,label) <-----被引用------- 第三层Continuation(这一层的局部变量,label)
这样来实现了一个叠叠乐。
这就是协程的挂起与恢复机制了。
五:异常的捕获
根据这个恢复机制还有它保存子函数的Result这些机制,我们可以明白,一个异常:
throw IllegalStateException之类(非CancellationCoroutineException,因为它会特殊处理,不应该主动抛出这个)
要么,在抛出的那个所处的协程体中抛出时就捕获,否则一直会向上传递直到根协程的ExceptionHandler处理它。
详情可参考https://medium.com/androiddevelopers/exceptions-in-coroutines-ce8da1ec060c
六:总结
- 挂起函数就像状态机,在函数开始和每次挂起函数调用之后都有一个可能的状态。
- 标识状态的标签和本地数据都保存在当前函数的Continuation中。
- 一个函数的continuation引用了另一个函数的continuation,因此,所有这些continuation都代表了我们恢复时使用的调用堆栈
本文首发于我的博客园:https://www.cnblogs.com/--here--gold--you--want/
本文参考了我的偶像Manuel,即博客封面那个蓝人的文章:https://medium.com/androiddevelopers/the-suspend-modifier-under-the-hood-b7ce46af624f
以及kotlin专家的文章:https://kt.academy/article/cc-under-the-hood#definition-3
本文作者:ou尼酱~~~
本文链接:https://www.cnblogs.com/--here--gold--you--want/p/15742603.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步