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 挂起点之后的逻辑。
网友评论