kotlin Coroutine原理

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 类型的参数,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 协程设计文档

你可能感兴趣的:(kotlin Coroutine原理)