Coroutine协程是kotlin实现的一种异步执行逻辑的方式,相对与传统的线程,协程更加简洁,高效,占用资源少。那协程到底是怎么实现异步的呢?
线程
在现在的操作系统中,线程是CPU调度的最少单元。所有的程序逻辑运行在线程之上。在Java API中, Thread是实现线程的基本类。它的内部实现是大量的 JNI 调用,因为线程的实现必须由操作系统直接提供支持。在 Android 平台上,Thread 的创建过程中,会调用 Linux API 中的 pthread_create 函数,这直接说明了 Java 层中的 Thread 和 Linux 系统级别的中的线程是一一对应的。
线程的问题是阻塞与运行两种状态之间的切换有相当大的资源开销,线程并不是一种轻量级资源,大量创建线程是对系统资源的一种消耗,而线程的阻塞调用会导致系统中存在大量因阻塞而不运行的线程,这对系统资源是一种极大的浪费。
协程
协程本质上可以认为是运行在线程上的代码块,协程提供的 挂起 操作会使协程暂停执行,而不会导致线程阻塞。而且协程是一种轻量级资源,一个应用中即使创建了上千个协程也不会造成太大的负担。
协程的是通过’suspend‘修饰符来修饰需要挂起的方法。suspend并不是Java的API,是kotlin通过编译器实现的。
CPS 变换
被 suspend 修饰符修饰的函数在编译期间会被编译器做特殊处理,这个特殊处理的第一步就是做CPS 变换。
CPS (Continuation-passing style)变换是一种编程风格,就是将控制流显式表示为continuation的一种编程风格. 简单来理解就是显式使用函数表示函数返回的后续操作。
例如:
suspend fun foo.await(): T
在编译期发生 CPS 变换之后:
fun foo.await(continuation: Continuation): Any?
CPS 变换后的函数多了一个 Continuation
interface Continuation {
val context: CoroutineContext
fun resumeWith(result: Result)
}
Continuation是一个抽象的概念,简单来说它包装了协程在挂起之后应该继续执行的代码;在编译的过程中,一个完整的协程被分割切块成多个续体。在 await 函数的挂起结束以后,它会调用 continuation 参数的 resumeWith 函数,来恢复执行 await 函数后面的代码。
方法经过CPS 变换之后,返回值类型变成了 Any?,这是因为这个函数在发生变换后,除了要返回它本身的返回值,还要返回一个标记——COROUTINE_SUSPENDED,而这个返回类型事实上是返回类型 T 与 COROUTINE_SUSPENDED 的联合类型。由于Kotlin 中没有联合类型,所以只好用最泛化的类型 Any? 来表示,而 COROUTINE_SUSPENDED 是一个标记,返回它的挂起函数表示这个挂起函数会发生事实上的挂起操作。
状态机
Continuation为了直接支持挂起(即使协程在挂起点中断执行而在适当的时机在恢复)操作,编译器在编译挂起函数时会将函数体编译为状态机。主要是为了性能考虑,避免多创建类和对象。
如:
val a = a()
val y = foo(a).await() // #1
b()
val z = bar(a, y).await() // #2
c(z)
编译之后生成的伪代码:
class 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 代表了当前状态机的状态,每一个续体(即挂起点中间的部分以及挂起点与函数头尾之间的部分)都各自对应了一个状态,当函数运行到每个挂起点时,label 的值都受限会发生改变,并且当前的续体(也就是代码中的this)都会作为实参传递给发生了 CPS 变换的挂起函数,如果这个挂起函数没有发生事实上的挂起,函数继续运行,如果发生了事实上的挂起,则函数直接 return。
由于 label 记录了状态,所以在协程恢复的时候,可以根据状态使用 goto 语句直接跳转至上次的挂起点并向后执行,这就是协程挂起的原理。另外,虽然 Java 中没有 goto 语句,但是 class 字节码中支持 goto。
续体拦截器
挂起函数在恢复的时候,理论上可能会在任何一个线程上恢复,有时我们需要限定协程运行在指定的线程,例如在Android中,更新 UI 的操作只能在 UI 主线程中进行。
android MainDispatcherLoader的实现:
// Main 调度器
@JvmStatic
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
// dispatcher 由 loadMainDispatcher() 函数创建
internal object MainDispatcherLoader {
@JvmField
val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()
private fun loadMainDispatcher(): MainCoroutineDispatcher {
......
}
}
// MainCoroutineDispatcher
public abstract class MainCoroutineDispatcher : CoroutineDispatcher() {
@ExperimentalCoroutinesApi
public abstract val immediate: MainCoroutineDispatcher
}
// CoroutineDispatcher
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
......
}
@InternalCoroutinesApi
public fun MainDispatcherFactory.tryCreateDispatcher(factories: List): MainCoroutineDispatcher =
try {
createDispatcher(factories)
} catch (cause: Throwable) {
MissingMainCoroutineDispatcher(cause, hintOnError())
}
/**
* @suppress
*/
@InternalCoroutinesApi
public object MissingMainCoroutineDispatcherFactory : MainDispatcherFactory {
override val loadPriority: Int
get() = -1
override fun createDispatcher(allFactories: List): MainCoroutineDispatcher {
return MissingMainCoroutineDispatcher(null)
}
}
// ContinuationInterceptor(续体拦截器)
public interface ContinuationInterceptor : CoroutineContext.Element {
companion object Key : CoroutineContext.Key
public fun interceptContinuation(continuation: Continuation): Continuation
public fun releaseInterceptedContinuation(continuation: Continuation<*>) {
/* do nothing by default */
}
// Performance optimization for a singleton Key
public override operator fun get(key: CoroutineContext.Key): E? =
@Suppress("UNCHECKED_CAST")
if (key === Key) this as E else null
// Performance optimization to a singleton Key
public override fun minusKey(key: CoroutineContext.Key<*>): CoroutineContext =
if (key === Key) EmptyCoroutineContext else this
}
ContinuationInterceptor,负责拦截协程在恢复后应执行的代码(即续体)并将其在指定线程或线程池恢复
在挂起函数的编译中,每个挂起函数都会被编译为一个实现了 Continuation 接口的匿名类,而续体拦截器会拦截真正挂起协程的挂起点的续体。在协程中调用挂起函数,挂起函数不一定会真正挂起协程
如:
launch {
val deferred = async {
// 异步逻辑
......
}
......
deferred.await()
......
}
在 deferred.await() 这行执行的时候,如果异步逻辑已经执行完成并取得了结果,那 await 函数会直接取得结果,而不会挂起协程。相反,如果网络请求还未产生结果,await 函数就会使协程挂起。续体拦截器只拦截真正发生挂起的挂起点后的续体,对于未发生挂起的挂起点,续体会被直接调用 resumeWith 这一类的函数而不需要续拦截器对它进行操作。除此之外,续体拦截器还会缓存拦截过的续体,并且在不再需要它的时候调用 releaseInterceptedContinuation 函数释放它。
参考:Kotlin 协程设计文档