java cps变换_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 函数释放它。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值