美文网首页
80.Kotlin协程挂起原理

80.Kotlin协程挂起原理

作者: SlideException | 来源:发表于2020-08-18 16:40 被阅读0次

    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 <T> CompletableFuture<T>.await():T
    

    在编译器发生CPS变换后

    fun <T> CompletableFuture<T>.await(continuation: Continuation<T>):Any?
    

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

    interface Continuation<in T> {
      val context:CoroutineContext
      fun resumeWith(result : Result<T>)
    }
    

    续体是一个比较抽象的概念,简单来说他包装了协程在挂起之后应该继续执行的代码,在编译的过程中,一个完整的协程被分割切块成一个又一个的续体, 在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 <anonymous_for_state_machine> 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<in T> {
        public val context: CoroutineContext
        public fun resumeWith(result: Result<T>)
    }
    

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

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

    相关文章

      网友评论

          本文标题:80.Kotlin协程挂起原理

          本文链接:https://www.haomeiwen.com/subject/sigfjktx.html