阿拉丁和灯

Thoughts, stories and ideas.



Kotlin Coroutine 释疑 —— 从外在到本质


背景

异步和并发,作为现在编程的核心问题之一,已经成为编程语言各显神通的地方。Kotlin提供了Coroutine这个有力的武器(基本介绍参看这里),来解决这方面的问题。

不同语言在异步和并发方面的解决方案,可以从两个重要的方面来看:

  1. 外在表现形式(语法关键字,API)
  2. 内在的实现(用线程,还是其他)

Kotlin Coroutine在这两方面都有创新,在借鉴其他语言的经验的基础上,取其优点,加上独特的改进,形成了自己的Coroutine实现和相应的suspend关键字及coroutine API。深入了解了Kotlin coroutine之后,你会感觉到它的设计非常精彩。但是,里面涉及到一些与我们的日常编程直觉不那么一样的地方,需要花一点时间仔细梳理理解一下,理解之后,一切就会变得非常自然和易于理解,便于使用了。

外在形式

代码示例

代码1

代码2

以上两段代码做的事情很容易理解,这里就不多说了。里面用到了Kotlin特有的关键字suspend,以及coroutine相关的API(launch, delay, async, await, runBlocking等),理解它们是理解coroutine的基础,下面就分别说明它们的含义。

suspend关键字和coroutine builder API

suspend 关键字标明 suspend function, suspend function只能被另一个suspend function直接调用,或者在一个coroutine 里(由coroutine builder创建;本质是具有一个有效的coroutine context)被调用。如何理解suspend函数呢?我的理解是,suspend函数是一个"两次返回"的函数,第一次先返回执行控制流,使得调用该函数的代码行之后的代码可以继续执行(这个时候suspend函数要做的事情实际上还没有完成);第二次当suspend函数要做的事情都做完的时候返回结果。因为有"两次返回",所以,调用它的时候必须指定如何处理第二次返回,这就是为什么只能在coroutine里调用它(把它包在runBlocking,或者async里面)。

Coroutine builders: 四大金刚launch, runBlocking, async和await。

  • launch。launch最简单,只负责启动,启动完就不管了,既不等待结束,也不关心结果。
  • runBlocking。顾名思义,runBlocking在运行代码块之后会阻塞当前coroutine,直到代码块运行完成,然后获取结果。
  • async和await。launch和runBlocking都相对简单,如果我要启动coroutine,然后立马返回,但是又想关心结果怎么办呢,用async。async会返回一个Deferred,T是结果类型,然后你可以做别的,需要的时候,用Deferred上的await()函数来等待结果。注意await()本身也是suspend函数,你需要把它放在coroutine里面(launch,runBlocking,async调用的代码块)里面,或者将代码所在的函数标为suspend。

下面的表格总结了四大金刚的特性和区别:

Builder 是否suspend(传播异步) 是否返回结果 返回值
runBlocking 代码块结果,基本类型
launch Job
async Defered
await 基本类型(T)

Builder是否suspend(传播异步)是否返回结果返回值runBlocking否是代码块结果,基本类型launch否否Jobasync否是Deferedawait是是基本类型(T)

注:Deferred是Job的子类型

除了上面四大金刚外,还有其他一些coroutine builder 函数。比如future,跟async的作用类似,只不过是用于Java互操作,产生的是Java CompletableFuture。可以在产生出来的东西上调用await()。还有delay,也是一个suspend function,意味着它会提前返回,到时间后再继续执行后面的代码。

理解本质

究竟如何理解coroutine以及相关的API呢?让我们回顾一下Java 8中的异步是怎么处理的。Java 8中的异步API主要是CompletableFuture类,"异步性"本身是传染性的,调用方一但启用异步,那么这种异步性就会一层层传播出去,除非调用阻塞的函数,比如CompletableFuture上的get(),来阻塞等待异步过程结束并返回结果。这就是我们说的异步性的"传染性"。而这种传染性在Kotlin Coroutine里面用一种不同的形式体现了,这就是suspend关键字,所有带suspend关键字的函数,都类似于CompletableFuture,是带异步性的,会传染的。调用他们(比如yield(), delay(), await())的代码,本身必须是suspend的,或者,用coroutine相关API(launch, runBlocking, async等)指定该如何处理这种异步性。

yield,delay, 以及await() 等是异步性的引入者,标志是他们的函数名前面带有suspend修饰符。在Java里面,异步是由线程来支撑的(作为背后的实现),而在Kotlin里面,异步是由Coroutine来支撑的,也就是说,yield将会创建一个Coroutine。yield/delay/await所创建的异步性,由suspend关键字传播出去。

async, launch,runBlocking,这些是异步性的处理者,标志是它们都接收一个suspend的Lambda表达式作为参数。需要注意的是,在Kotlin当中,对于异步性的默认处理是同步调用,如果需要让异步性发挥效力,要用async函数来调用。调用得到的是Defered。

await所起的作用类似于Java 8 CompletableFuture上的thenApply等方法所起的作用,就是维持住异步性的同时,让处理代码可以接收一个一般类型,而不是Defered类型。这样就可以像写正常业务逻辑一样,不用考虑异步性。

API实现

上面讲的虽然很准确,但有点抽象。让我们看看代码,就明白是怎么回事了。

1 - yield

看一下yield的实现:

可以看到,里面调用了suspendCoroutineOrReturn函数,而suspendCoroutineOrReturn函数又做了什么呢?

可以看到,这个方法调用了suspendCoroutineUninterceptedOrReturn,而这个suspendCoroutineUninterceptedOrReturn已经是内部代码了,在类库代码里面已经不可见了,也就是说,到这里就深入了Kotlin Coroutine的实现内部,我们可以知道,这里就是创建协程(Coroutine)的地方了。

2 - async

3 - await

4 - runBlocking

5 - launch

相关文件

以下列出包含上面这些函数的代码文件,方面大家查找:

这些文件所在路径是
kotlinx.coroutines下面的common和core两个子模块,kotlinx.coroutines.experimental包下面。

内在实现

Coroutine API背后使用的是coroutine来实现,用的是基于传播continuation(改良版的callback)的实现。而Coroutine实现的本质是一个状态机,状态机的状态就是当前执行的代码的位置,而状态机的事件就是coroutine的启动,完成(出错等)。编译器维护了一个状态机,在某个异步代码结束的时候,根据这个状态机决定下一步应该返回到哪里去继续执行。具体的机制在这个视频里面讲解的非常清楚,有兴趣的同学可以看看:

KotlinConf 2017 - Deep Dive into Coroutines on JVM by Roman Elizarov

受控执行

除了以上演示的一次性的异步执行之外,我们还可以用Coroutine实现消费者和生产者协同,生产者受控生产数据的代码,示例如下:

这里面用到了yield函数,但是注意这个yield方法是SequenceBuilder的成员函数,跟前面提到的全局的yield函数有所不同,具体见下面描述。另外一个这里的重要方法是buildSequence,代码如下:

可以看到buildSequence接受一个suspend Lambda表达式作为参数,所做的事情是构建一个Sequence,而重点是iterator的nextStep,实际上是指向了一个由createCoroutineUnchecked创建的coroutine。那么在sequence的next方法被调用的时候又会发生什么呢?看代码:

从图中可以看到,hasNext和yield互相配合,yield挂起coroutine(图中13),而hasNext调用resume(图中5)来让coroutine继续运行。

参考资料

KotlinConf 2017 - Introduction to Coroutines by Roman Elizarov
https://www.youtube.com/watch?v=_hfBv0a09Jc

KotlinConf 2017 - Deep Dive into Coroutines on JVM by Roman Elizarov
https://www.youtube.com/watch?v=YrrUCSi72E8

Kotlin Coroutine教学课程
Kotlin: Using Coroutines - by PluralSight



View or Post Comments