80.Kotlin协程挂起原理

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,932评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,554评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,894评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,442评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,347评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,899评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,325评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,980评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,196评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,163评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,085评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,826评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,389评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,501评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,753评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,171评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,616评论 2 339