kotlin协程中状态机视角看挂起与恢复

5 阅读3分钟

kotlin协程中状态机视角看挂起与恢复

挂起

在状态机的挂起点返回了COROUTINE_SUSPENDED,停止挂起点后续执执行。挂起的实质是把状态机某个挂起点后续的执行给暂停了。

恢复

状态机实例在挂起点内部被获取到,挂起点代码执行完毕以后调用状态机的resume()系列方法,继续执行挂起点后续 的执行。恢复的实质是继续执行某个挂起点后续的代码。

什么代码会被编译成编译成状态机

  1. 带suspend关键字的函数
  2. 带suspend关键字的非inline方法,并且该方法内部调用了挂起方法,并且不在最后一行(最后一行需要有有效调用)

以上情况编译成的状态机都是BaseContinuationImpl的子类。

状态机的创建时机

  • 当进入2.和3.情况的方法时,会创建一个状态机,在这个方法内会复用这个状态机。
  • 1.情况的状态机暂时不知道所有的创建时机

状态机视角下的执行情况

// “top compeletion”恢复之前任意时刻下状态机分布:  
// “C”表示当前状态机的completion字段
top compeletion
               <--C-- 状态机1
                            <--C-- 状态机2
                                    ....
                                          <--C-- 状态机N

completion有两种:

  1. BaseContinuationImpl子类,表示一个状态机
  2. 非BaseContinuationImpl,例如DebugProbesImpl$CoroutineOwner(delegate:StandaloneCoroutine)

例子

fun main() {
    GlobalScope.launch {
        // A1
        like()
    }
    while (true) {
    }
}

private suspend fun like() {
    println("before like")
    // A2
    love()
    println("after like")
}

private suspend fun love() {
    println("before love")
    // A3
    
    // 只要挂起方法内有这个方法的调用,挂起方法内部必生成状态机器
    suspendCoroutineUninterceptedOrReturn<String> {
        thread {
            it.resume("hah")// B1
        }
        
        COROUTINE_SUSPENDED // A4
    }
    println("after love")
}

当协程执行起来以后block状态机的resumeWith()方法会被调用,接着执行它的invokeSuspend(),然后执行到“A1”,此时:

// 将断点打在BaseContinuationImpl#resumeWith()中然后查看completion可以看到如下分布
DebugProbesImpl$CoroutineOwner(delegate:StandaloneCoroutine)
     <--C-- block状态机(executing)

继续进入like()方法,执行到“A2”,此时like()方法内部的状态机会被创建:

DebugProbesImpl$CoroutineOwner(delegate:StandaloneCoroutine)
     <--C-- block状态机(executing)
                      <--C-- like(executing)

类似的,继续执行到“A3”:

DebugProbesImpl$CoroutineOwner(delegate:StandaloneCoroutine)
     <--C-- block状态机(executing)
                      <--C-- like(executing)
                                 <--C-- love(executing)

此时观察反编译后的java代码:

  1. love()方法在“A4”返回“COROUTINE_SUSPENDED”后,love状态机挂起
  2. like()方法发现love()返回“COROUTINE_SUSPENDED”后,返回这个值,like状态机挂起
  3. block状态机的invokeSuspend()方法发现like()返回“COROUTINE_SUSPENDED”后,返回这个值,block状态机挂起
  4. 接着block状态机的resumeWith()方法返回。

当某个状态机挂起以后,它的completion如果指向一个状态机,这个状态机也会紧接着挂起(挂起点后面的代码暂时不执行了呀)。

此时查看状态机分布:

DebugProbesImpl$CoroutineOwner(delegate:StandaloneCoroutine)
     <--C-- block状态机(suspend)
                      <--C-- like(suspend)
                                 <--C-- love(suspend)

此时有3个状态机,都处于挂起的状态。 在后续某个时刻,代码执行到“B1”,恢复love状态机的执行。

此时查看状态机分布:

DebugProbesImpl$CoroutineOwner(delegate:StandaloneCoroutine)
     <--C-- block状态机(suspend)
                      <--C-- like(suspend)
                                 <--C-- love(executing)

观察反编译后的java代码:

  1. 此时后续会调用到love.resume()->love.resumeWith()->love.invokeSuspend()
  2. 由于love状态机后续执行中没有挂起点了,love.invokeSuspend()直接返回,love状态机中由程序员写的代码一执行完毕。
  3. 由于love的completion为like状态机,他是BaseContinuationImpl类型的,所以这里不会调用like.resume()系列方法二是【直接调用】 like.invokeSuspend()方法来恢复like状态机的执行(unrolling recursion via loop)。

此时查看状态机分布:

DebugProbesImpl$CoroutineOwner(delegate:StandaloneCoroutine)
     <--C-- block状态机(suspend)
                      <--C-- like(executing)
                                 <--C-- love(terminal)

类似的:

  1. like状态机中后续执行中没有挂起点了,like.invokeSuspend()直接返回
  2. 接着【直接调用】block.invokeSuspend()方法恢复block状态机的执行。 此时查看状态机分布:
DebugProbesImpl$CoroutineOwner(delegate:StandaloneCoroutine)
     <--C-- block状态机(executing)
                      <--C-- like(terminal)
                                 <--C-- love(terminal)

类似的:

  1. block.invokeSuspend()执行完毕
  2. 此时block的completion为DebugProbesImpl$CoroutineOwner,并不是状态机了,所以此时会调用ower.resumeWith()恢复后续执行。
  3. 接着调用StandaloneCoroutine.resumeWith()方法

此时查看状态机分布:

DebugProbesImpl$CoroutineOwner(delegate:StandaloneCoroutine)
     <--C-- block状态机(terminal)
                      <--C-- like(terminal)
                                 <--C-- love(terminal)

至此,例子中的状态机全部terminal。

另外

在刚刚的例子中,如果love状态机恢复执行以后再调用一个内部会挂起一次的funny()方法,在执行到挂起之前,状态机分布变为:

DebugProbesImpl$CoroutineOwner(delegate:StandaloneCoroutine)
     <--C-- block状态机(suspend)
                      <--C-- like(suspend)
                                 <--C-- love(suspend)
                                            <--C-- funny(executing)

执行到funny()方法的挂起点时,状态机分布变为:

DebugProbesImpl$CoroutineOwner(delegate:StandaloneCoroutine)
     <--C-- block状态机(suspend)
                      <--C-- like(suspend)
                                 <--C-- love(suspend)
                                            <--C-- funny(suspend)

总结

协程执行时可能有多个对应的状态机存在,由程序员编写的代码会被编译器分布到各个状态机中,在运行时被执行。

当某个状态机,假设名叫A,中代码返回 COROUTINE_SUSPENDED时表示有挂起发生了,这个状态机以及其他状态机在它们当前挂起点后的代码暂时停止执行。所以“挂起”实际发生的位置是在状态机上。

当A.resume()系列方法被调用以后,会继续执行刚刚挂起点后续的代码。所以“恢复”实际上也发生在状态机上。