80.Kotlin协程挂起原理

https://blog.csdn.net/suyimin2010/article/details/91125803
https://www.jianshu.com/p/2979732fb6fb
依赖
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
// Kotlin Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0-RC1'
挂起函数:

suspend fun  CompletableFuture.await():T

在编译器发生CPS变换后

fun  CompletableFuture.await(continuation: Continuation):Any?

编译器对挂起函数的第一个改变就是对函数签名的改变,这种改变称为CPS(续体传递风格)变换。
我们看到在CPS变换后的函数多了一个Continuation类型的参数,Continuation意思是续体,他的声
明如下:

interface Continuation {
  val context:CoroutineContext
  fun resumeWith(result : Result)
}

续体是一个比较抽象的概念,简单来说他包装了协程在挂起之后应该继续执行的代码,在编译的过程中,一个完整的协程被分割切块成一个又一个的续体, 在await函数挂起结束以后,他会调用continuation参数的resumeWith函数,来恢复执行await函数后的代码。
CPS变换后的返回值类型变成了Any?,这是因为变化后除了要返回它本身的一个值,还要返回一个标记COROUTINE_SUSPENDED,而这个返回类型实际上是返回类型T与COROUTINE_SUSPENDED的联合类型,Kotlin里没有联合类型的概念,也没有加入这种语法计划,就用了Any。COROUTINE_SUSPENDED是一个标记,返回他的挂起函数表示这个函数会发生事实上的挂起操作。
续体拦截器ContinuationInterceptor,负责拦截协程在恢复后应执行的代码(即续体)并将其在指定的线程或者线程池恢复。
在挂起函数的编译中,每个挂起函数都会被编译为一个实现了Continuation接口的匿名类,而续体拦截器会拦截真正挂起协程的挂起点的续体,在协程中调用挂起函数,不一定会真正挂起协程。
ex:

launch {
    val deferred = async {
        // 发起了一个网络请求
        ......
    }
    // 做了一些操作
    ......
    deferred.await()
    // 后续的一些操作
    ......
}

在deferred.await()执行的时候,如果网络请求已经获取到了结果,那么await函数会直接取得结果,而不会事实上的挂起协程,相反,如果网络请求还未产生结果,await函数就会使协程挂起。续体拦截器只拦截真正发生挂起的挂起点后的续体,对于未发生挂起的挂起点,续体会被直接调用resumeWith方法而不需要续体拦截器对它进行操作。releaseInterceptedContinuation函数释放它。

状态机

协程在编译挂起函数时会将函数体编译为状态机,这样做的好处在于避免创建过多的类和对象,是处于一种性能上的考虑。
ex:

val a = a()
val y = foo(a).await() // 挂起点 #1
b()
val z = bar(a, y).await() // 挂起点 #2
c(z)

这一段挂起函数内部的代码,它拥有两个挂起点;在编译后生成如下的伪 Java 字节码:

lass  extends SuspendLambda<...> {
    // 状态机当前状态
    int label = 0
    // 协程的局部变量
    A a = null
    Y y = null
    void resumeWith(Object result) {
        if (label == 0) goto L0
        if (label == 1) goto L1
        if (label == 2) goto L2
        else throw IllegalStateException()
      L0:
        // 这次调用,result 应该为空
        a = a()
        label = 1
        result = foo(a).await(this) // 'this' 作为续体传递
        if (result == COROUTINE_SUSPENDED) return // 如果 await 挂起了执行则返回
      L1:
        // 外部代码传入 .await() 的结果恢复协程 
        y = (Y) result
        b()
        label = 2
        result = bar(a, y).await(this) // 'this' 作为续体传递
        if (result == COROUTINE_SUSPENDED) return // 如果 await 挂起了执行则返回
      L2:
        // 外部代码传入 .await() 的结果恢复协程
        Z z = (Z) result
        c(z)
        label = -1 // 没有其他步骤了
        return
    }          
}    

一个挂起函数会被编译为一个匿名类,这个匿名类中的一个函数实现了这个状态机,成员变量label代表了当前状态机的状态,每一个续体(即挂起点中间部分以及挂起点与函数头尾之间的部分)都各自对应了一个状态,当函数运行到每个挂起点时,lable的值都受限会发生改变,并且当前的续体都会作为实体参数传递给发生了CPS变换的挂起函数,如果这个挂起函数没有发生事实上的挂起,函数继续运行,如果发生了事实上的挂起,则函数直接return。
由于label记录了状态,所以在协程恢复的时候,可以根据状态使用goto语句直接跳转至上次的挂起点并向后执行,这就是协程挂起的原理。顺便提一句,虽然 Java 中没有 goto 语句,但是 class 字节码中支持 goto。

挂起函数的工作原理:
挂起函数或挂起Lambda表达式调用时,都有一个隐式的参数额外传递,这个参数是continuation类型,封装了协程恢复后的执行的代码逻辑。

Continuation类类似于一个通用的回调接口:

public interface Continuation {
    public val context: CoroutineContext
    public fun resumeWith(result: Result)
}

协程内部实现使用状态机来处理不同挂起点,每一个挂起点和初始挂起点对应的continuation都会转化为一种状态,协程恢复只是跳转到下一种状态中,挂起函数将执行过程分为多个continuation片段,并且利用状态机的方式保证各个片段是顺序执行的。
挂起函数可能会也可能不会挂起协程。
挂起函数不会阻塞线程。
挂起函数恢复协程后运行在Dispatcher指定的线程,挂起恢复后运行在哪个线程完全由所调用的挂起函数决定

await()挂起函数恢复协程的原理是,将 launch 协程封装为 ResumeAwaitOnCompletion 作为 handler 节点添加到 aynsc 协程的 state.list,然后在 async 协程完成时会通知 handler 节点调用 launch 协程的 resume(result) 方法将结果传给 launch 协程,并恢复 launch 协程继续执行 await 挂起点之后的逻辑。

你可能感兴趣的:(80.Kotlin协程挂起原理)