切换菜单
搜索
个人笔记云
首页
java
spring
springmvc
python
使用教程
笔记管理
搜索
登录/注册
好物分享
退出
搜索
80.Kotlin协程挂起原理
2021-10-15
260
**参考视频教程:** [**协程原理从入门到精通 每个后端开发都需要掌握的高性能开发技术 **](http://www.notescloud.top/goods/detail/1366) [https://blog.csdn.net/suyimin2010/article/details/91125803](https://links.jianshu.com/go?to=https%3A%2F%2Fblog.csdn.net%2Fsuyimin2010%2Farticle%2Fdetails%2F91125803)
依赖 // 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 挂起点之后的逻辑。
教程分类
热门视频教程
热门文章
热门书籍推荐