Kotlin协程原理解析

Kotlin协程实际上是所谓的stackless协程,即没有在线程之上实现类似线程栈的结构,可以认为是一种kotlin语言层面支持的 线程调度框架,使用这个框架,我们可以省去手动书写callback,使代码看上去是同步的

使用了协程以后,原本需要回调实现的非阻塞功能可以用阻塞的方式去写,比如:

launch {
    // 异步读时挂起
    val bytesRead = inChannel.aRead(buf)
    // 读完成后才执行这一行
    ...
    ...
    process(buf, bytesRead)
    // 异步写时挂起
    outChannel.aWrite(buf)
    // 写完成后才执行这一行
    ...
    ...
    outFile.close()
}
// 协程挂起后,线程继续执行
println("thread continue")

执行上述代码段时,在协程运行到val bytesRead = inChannel.aRead(buf)这一句时挂起,线程继续执行协程外的代码,输出thread continue

那么,协程是如何做到这样的“黑科技”呢?网上搜一圈,基本上都会搜到CPS(Continuation-Passing-Style, 续体传递风格),听起来挺玄乎的,什么是CPS呢?

说的简单点,其实就是函数通过回调传递结果,让我们看看这个例子

class Test {
    public static long plus(int i1, int i2) {
        return i1 + i2;
    }
    public static void main(String[] args) {
        System.out.println(plus(1, 2));
    }
}

这个例子是常规的写法,函数plus的结果通过函数返回值的形式返回并进行后续处理(这里仅仅打印),如果把例子改写成CPS风格,则是

class Test {
    interface Continuation {
        void next(int result);
    }
    public static void plus(int i1, int i2, Continuation continuation) {
        continuation.next(i1 + i2);
    }
    public static void main(String[] args) {
        plus(1, 2, result -> System.out.println(result));
    }
}

很简单吧?这就是CPS风格,函数的结果通过回调来传递

协程里通过在CPS的Continuation回调里结合状态机流转,来实现协程挂起-恢复的功能,来看下面的例子

假设我们有一个扩展的挂起函数:

suspend fun <T> CompletableFuture<T>.await(): T

在编译过后,其函数签名将变成

fun <T> CompletableFuture<T>.await(continuation: Continuation<T>): Any?

可以看到,入口参数多了一个Continuation类型,这个就是CPS续体,其实也就是我们上面说的回调

现在我们再假设有这么段代码

val a = a()
val y = foo(a).await() // 挂起点 #1
b()
val z = bar(a, y).await() // 挂起点 #2
c(z)

这段代码必须在协程里面执行(launch{}或者async{}等的lambda里),且调用了两个await()挂起函数,其编译后将变成这样的伪代码(因为是编译器直接生成字节码)


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:
        // 这次调用,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
    }         
}

可以看到,以两个挂起函数的调用点为分界,生成了一个具有3个状态的状态机类

现在,当协程开始时,我们调用了它的 resumeWith() —— label 是 0,然后我们跳去 L0,接着我们做一些工作,将 label 设为下一个状态—— 1,调用 .await(),如果协程执行挂起就返回。当我们想继续执行时,我们再次调用 resumeWith(),现在它继续执行到了 L1,做一些工作,将状态设为 2,调用 .await(),同样在挂起时返回。下一次它从 L3 继续,将状态设为 -1,这意味着"结束了,没有更多工作要做了"。

其实编译后的代码就是利用这个生成的类作为续体(Continuation)传递了挂起点前后的中间结果,并且通过状态机,来记忆协程恢复后应该执行哪段代码


续体状态机流转

好了,这就是协程挂起和恢复的实现方式了,可以看到,我们并不能把Kotlin协程当作是所谓的“轻量级线程”来解释,它更像是一个以同步方式去写异步方法,并帮助开发者生成回调方法(CPS风格)的线程调度框架,当然本文只分析了协程的挂起和恢复,还有协程的取消操作,上下文(可用于限定协程在某个线程工作)等等功能,原理解析推荐阅读:
Kotlin中文官网的协程设计文档(中文)
Bennyhuo的破解Kotlin协程系列(中文)

协程的使用推荐阅读:
扔物线的Kotlin教学视频及文章(协程部分)
google codelabs kotlin coroutines guide(手把手教你写协程)

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