Kotlin学习之-6.4 Coroutines
在Kotlin V1.1中Coroutines 还处在实验阶段
有些接口会做一些耗时的操作例如网络IO请求,文件IO, CPU或者GUP密集的工作等,并且要求调用者阻塞知道操作完成。Coroutines提供了一种能够替换避免阻塞线程的方法并且代价更小、控制性更好的操作:suspension of a coroutine
Coroutines通过把复杂的管理放到代码库中来简化异步编程。程序的逻辑可以在Coroutine中线性的表达,然后使用的库会帮我们解决异步处理的问题。库可以把我们相关的代码包装成回调,订阅给相关的事件,分配到不同线程上执行,最终代码仍然保持和线性顺序执行一样简单。
在其他语言中有很多异步机制可以用来实现Kotlin的coroutines。这包括C#和ECMASCript语言中的async/await
,Go语言中channels
和select
,还有C#和Python中的generator/yield
。
阻塞和挂起
通常来说,coroutines是那些可以被挂起但不需要阻塞线程的操作。阻塞线程是非常昂贵的,尤其是在高负载的情况下,因为只有一个数量相对小的线程在实际中持续运行,因此阻塞其中的一个会导致一些重要的工作被延迟。
另一方面,Coroutine挂起几乎是没有损耗的。没有上下文切换或者其他操作系统的需求。并且,挂起还可以通过一个用户库来控制,作为库的作者,我们可以决定在挂起时要做什么并且可以根据我们的需求来优化、添加日志或者拦截相应的操作。
另外一个区别是coroutines不能被随机的指令挂起,而只能在一些特殊的挂起点处挂起,他们会调用特殊标记的函数。
挂起函数
挂起发生在我们调用一个标记了suspend
的函数的时候:
suspend fun doSomething(foo: Foo): Bar {
}
这样的函数叫作挂起函数,因为调用这些函数可能会挂起一个coroutine(库可以决定执行是否挂起,如果调用的结果已经可用的话)。 挂起函数可以和普通函数一样有参数和返回值,但是他们只能在coroutines 中或者其他挂起函数中调用。实际上,要启动一个coroutine,必须至少有一个挂起函数,并且通常是匿名,例如一个挂起lambda表达式。 我们看一个例子,一个简化的async()
函数(来自kotlinx.coroutines
库)
fun<T> async(block: suspend () -> T)
这里,async()
是一个普通函数(不是一个挂起函数),但是block
参数有一个suspend
修饰的函数类型:suspend () -> T
。因此,当我们传递一个lambda表达式给async()
的时候,他就是一个挂起lambda表达式,我们可以在它里面调用一个挂起函数。
async {
doSomething(foo)
}
为了继续类比,await()
函数可是是一个挂起函数(因此可以在async()
代码块中调用),它会挂起一个coroutine知道有些计算完成并且返回。
async {
val result = computation.await()
}
更多关于真正async/await
函数工作的信息可以在kotlinx.coroutines
中找到,点击这里。
注意挂起函数await()
和doSomething
不在在一个普通函数被调用,如main()
函数
fun main(args: Array<String>) {
doSomething()
}
还有要注意的是挂起函数可以是虚函数,并且当他们被复写的时候,suspend
修饰符必须要指明。
interface Base {
suspend fun foo()
}
class Derived: Base {
override suspend fun foo() { }
}
@RestrictsSuspension 注解
扩展函数和lambda表达式也可以标记成suspend,就想普通函数一样。这使得可以创建用户可以扩展的DSL和其他API。在有些情况下库的作者需要放置用户添加新的方式来挂起一个coroutine。
为了达到这个,可以使用@RestrictsSuspension
注解。当一个接收类或者接口R
是用它注解的时候,所有挂起的扩展函数都需要代理到其他R的成员或者其他扩展上。因为扩展不能相互无线代理(这样程序就无法结束了), 这保障了所有的挂起都发生在调用R
的成员的时候,而这些是库的作者可以完全控制的。
这和一种少见的情况相关,当每一个挂起在库中是用一个特殊的方式处理的。例如,当通过buildSequence()
函数来实现生成器的时候,我们需要确保任何挂起调用都在coroutine并用调用yield()
或者yieldAll()
来结束,而没有任何其他函数。 这就是SequenceBuilder
是用@RestrictsSuspension
来注解的原因。
@RestrictsSuspension
public abstract class SequenceBuilder<in T> {
}
源代码见Github
coroutine 的内部工作
这里我们不会完全的讲解coroutine的背后的工作原理,但是简要地知道发生了什么还是很重要的。
Coroutines的实现是完全通过一种编译技术,(不需要VM 和OS 的支持)并且挂起是通过代码转换来实现的。 基本上,所有挂起函数(可能会优化,但是这里先不详细展开)都转换成一个状态机它的状态对应了挂起调用的不同状态。然后,在挂起之前,下一个状态存储在一个编译器生成的类的对象和其他相关的局部变量中。当coroutine恢复执行时,局部变量会恢复并且状态机会从挂起之前的状态继续执行。
一个挂起的coroutine可以存储起来并当做一个对象传递,保留它的挂起状态和局部变量。这种类型的对象是Continuation
,并且所有代码转换都描述在这里和经典的Continuation-passing style相对应。结果就是,具体实现中挂起函数使用一个额外的Continuation
类型的参数。
更多关于cotoutines如何实现的细节可以在设计文档中找到。关于其他语言中async/await
的解释相关,尽管他们的语言特性使得不会像Kotlin的coroutine这样通用。
coroutine的实验状态
coroutine的设计还是实验性的,这意味着它可能会在将来的发布中变化。当用Kotlin v1.1编译coroutine的时候,会有一个默认的警告:coroutine特性是实验性的
。 想要移除这个警告,你需要标明opt-in
标志位。
由于他的实验状态,在标准库中和coroutine相关的API都放在了kotlin.coroutines.experimental
包下。 当设计完成时,实验状态将被移除,最终的API会被移动到kotlin.coroutines
包下,并且实验状态的包会被保留下来用来向后兼容旧版本。
重要提示:我们建议库作者遵循同样的规范:添加”experimental” 后缀到你的包后面,如果它暴露了基于coroutine的API,这样你的库就能保持二进制的兼容性。当正式API发布时,遵循以下步骤:
- 拷贝所有API 到
com.example
下(不带experimental
后缀的包名) - 保留实验的包,为了向后兼容性。
这将会减少用户的移植问题。
标准API
coroutine主要包含3部分:
- 语言支持(挂起函数,如上所述)
- Kotlin标准库底层核心API
- 可以直接被用户使用的高层API
底层API: kotlin.coroutines
底层API想对很小并且永远不应该用来做除了构建高层库的事情。它包含两个主要的包:
kotlin.coroutines.exmperimental
,包含主要类型和基础元素,例如
- createCoroutine()
- startCoroutine()
- suspendCoroutine()
kotlin.coroutines.experimental.intrinsics
包含更底层的核心接口例如suspendCoroutineOrReturn
更多关于如何使用这些接口的细节,点击这里
kotlin.coroutines中的生成器接口
在kotlin.coroutines.experimental
包中仅有的应用程序级别函数是
这些接口和kotlin-stadlib
一起发布是因为他们和序列相关。实际上,这些函数实现了生成器,例如提供一种简单构造延迟序列的方法:
import kotlin.coroutines.experimental.*
fun main(args: Array<String>) {
val fibonacciSeq = buildSequence {
var a = 0
var b = 1
yield(1)
while (true) {
yield(a + b)
val tmp = a + b
a = b
b = tmp
}
}
// Print the first five Fibonacci numbers
println(fibonacciSeq.take(8).toList())
}
// output
[1, 1, 2, 3, 5, 8, 13, 21]
这生成了一个延迟加载,可能是无线的斐波那契数列通过创建一个能够生成连续斐波那契数字的函数的coroutine。当遍历这样的序列时,每一步迭代器执行生成下一个数字。因此,我们可以从列表中获取任意有限数量的数列, 例如fibonacciSeq.take(8).toList()
返回[1, 1, 2, 3, 5, 8, 13, 21]
。并且coroutine代价很低足可以让这种方式非常实用。
为了掩饰序列到底是如何延迟加载的,我们在调用buildSequence()
时打印一些调试信息:
import kotlin.coroutines.experimental.*
fun main(args: Array<String>) {
val lazySeq = buildSequence {
print("START ")
for (i in 1..5) {
yield(i)
print("STEP ")
}
print("END")
}
// Print the first three elements of the sequence
lazySeq.take(3).forEach { print("$it ") }
}
// output
START 1 STEP 2 STEP 3
运行上面的代码发现如果我们打印前3个元素,那么数字会和STEP
的输出信息交织在一起。这意味着实际上计算是延迟的。为了打印1
,我们只需要执行到第一个yield(i)
,并且START
也会打印出来。接下来,要输出2
,我们需要执行到下一个yield(i)
,这会输出STEP
.输出3的情况类似。最后一个STEP
和END
不会被输出,因为我们没有请求更多的序列元素。
可以使用yieldAll()
函数来一次生成一个集合
import kotlin.coroutines.experimental.*
fun main(args: Array<String>) {
val lazySeq = buildSequence {
yield(0)
yieldAll(1..10)
}
lazySeq.forEach { print("$it ") }
}
// output
0 1 2 3 4 5 6 7 8 9 10
函数buildIterator()
和buildSequence()
工作方式很像,但是返回一个延迟的迭代器。
我们可以给buildSequence()
添加自定义逻辑,通过写挂起扩展方法给SequenceBuilder
类。
import kotlin.coroutines.experimental.*
suspend fun SequenceBuilder<Int>.yieldIfOdd(x: Int) {
if (x % 2 != 0) yield(x)
}
val lazySeq = buildSequence {
for (i in 1..10) yieldIfOdd(i)
}
fun main(args: Array<String>) {
lazySeq.forEach { print("$it ") }
}
// output
1 3 5 7 9
其他高级接口: kotlinx.coroutines
只有和coroutine相关的核心接口可以在Kotlin标准库中存在。这包含了大部分的核心基础内容和接口,这些可能会被coroutine相关的库使用。
大多数应用层基于coroutines的接口都发布在一个单独的库中:kotlinx.coroutines
。这个库包含:
- 平台无关的异步编程接口
kotlinx-coorutines-core
- 这个模块包含Go语言风格的支持
select
操作的管道和其他一些方便的基础元素 - 这个库的详细使用说明在这里
- 这个模块包含Go语言风格的支持
- 基于JDK 8 的
CompletableFuture
的接口:kotlinx-coroutines-jdk8
- 基于JDK 7 的非阻塞IO(NIO)接口:
kotlinx-coroutines-nio
- 支持Swing(
kotlinx-coroutins-swing
)和JavaFx(`kotlinx-coroutines-javafx) - 支持RxJava:
kotlinx-coroutines-rx
这些库既提供方便的API使得普通工作更加简单,并且还提供了端到端的如何构建基于coroutine库的例子
PS,我会坚持把这个系列写完,有问题可以留言交流,也关注专栏Kotlin for Android Kotlin安卓开发