目录
一. 协程的挂起、恢复和调度的设计思想
被 suspend 修饰符修饰的函数在编译期间会被编译器做特殊处理:CPS(续体传递风格)变换,它会改变挂起函数的函数签名。
suspend fun <T> CompletableFuture<T>.await(): T
会转变成
fun <T> CompletableFuture<T>.await(continuation: Continuation<T>): Any?
编译器对挂起函数的第一个改变就是对函数签名的改变,这种改变被称为 CPS(续体传递风格)变换。
我们可以看到,函数变换之后多了一个参数Continuation,声明如下:
interface Continuation<in T> {
val context: CoroutineContext
fun resumeWith(result: Result<T>)
}
Continuation 包装了协程在挂起之后应该继续执行的代码;在编译的过程中,一个完整的协程可能会有多个挂起点 (suspension point) , 挂起点把协程分割切块成一个又一个续体。在 await 函数的挂起结束以后,它会调用 continuation 参数的 resumeWith 函数,来恢复执行 await 函数后面的代码。
值得一提的是,除了会返回一个本身的返回值,还会返回一个标记,COROUTINE_SUSPENDED,返回它的挂起函数表示这个挂起函数会发生事实上的挂起操作。什么叫事实上的挂起操作呢?比如:
launch {
val deferred = async {
// 发起了一个网络请求
......
}
// 做了一些操作
......
deferred.await()
// 后续的一些操作
......
}
在 deferred.await() 这行执行的时候,如果网络请求已经取得了结果,那 await 函数会直接取得结果,而不会事实上的挂起协程。
明白了这么多概念之后,我们看看一个具体的例子:
val a = a()
val y = foo(a).await() // 挂起点 #1
b()
val z = bar(a, y).await() // 挂起点 #2
c(z)
这里有两个挂起点,编译后可以看到生成的伪字节码:
class <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:
a = a()
label = 1
// 'this' 作为续体传递
result = foo(a).await(this)
// 如果 await 挂起了执行则返回
if (result == COROUTINE_SUSPENDED) return
L1:
// 外部代码调用resumeWith
y = (Y) result
b()
label = 2
result = bar(a, y).await(this)
if (result == COROUTINE_SUSPENDED) return
L2:
Z z = (Z) result
c(z)
// label = -1 代表已经没有其他的步骤了
label = -1
return
}
}
在这段伪代码中,我们很容易理解它的实现逻辑:L0 代表挂起点1之前的续体,首先goto L0开始,直到调用挂起点1的 result = foo(a).await(this) 方法,this就是续体,如果 await 没挂起,直接使用结果跳入L1中;如果挂起了则直接返回,await 方法执行完后,调用 await 方法体中的 Continuation 对象,调用它的 resumeWith ,goto L1,依次类推。