很多其它语言中的异步模型都可以用 Kotlin 协程实现为库。比如 C# ECMAScipt 中的  ,Go 语言中的 channels 和 ,以及 C# 和 Python 的 generators/yield 模型。下面的描述会详细解释提供这些结构的库。

    一般来说,协程是一种可以不阻塞线程但却可以被挂起的计算过程。线程阻塞总是昂贵的,尤其是在高负载的情形下,因为只有小部分的线程是实际运行的,因此阻塞它们会导致一些重要的任务被延迟。

    而协程的挂起基本没有什么开销。没有上下文切换或者任何的操作系统的介入。最重要的是,挂起是可以背用户库控制的,库的作者可以决定在挂起时根据需要进行一些优化/日志记录/拦截等操作。

    另一个不同就是协程不能被任意的操作挂起,而仅仅可以在被标记为 挂起点 的地方进行挂起。

    挂起函数

    当一个函数被 suspend 修饰时表示可以被挂起。

    这样的函数被称为 挂起函数,因为调用它可能导致挂起协程(库可以在调用结果已经存在的情形下决定取消挂起)。挂起函数可以想正常函数那样接受参数返回结果,但只能在协程中调用或着被其他挂起函数调用。事实上启动一个协程至少需要一个挂起函数,而且常常时匿名的(比如lambda)。下面这个例子是一个简单的async() 函数(来自kotlinx.coroutines 库):

    1. fun <T> async(block: suspend() -> T)

    这里的 async()只是一个普通的函数(不是挂起函数),但 block 参数是一个带有 suspend 修饰的函数类型,所以当传递一个 lambda 给async()时,这会是一个挂起 lambda ,这样我们就可以在这里调用一个挂起函数了。

    1. async{
    2. doSomething(foo)
    3. }

    继续类比,await() 函数可以是一个挂起函数(因此在 await(){} 语句块内仍然可以调用),该函数会挂起协程直至指定操作完成并返回结果:

    更多关于 async/await 原理的内容请看

    注意 await()doSomething() 不能在像 main() 这样的普通函数中调用:

    1. fun main(args: Array<String>) {
    2. doSomething() // 错误:挂起函数从非协程上下文调用
    3. }

    还有一点,挂起函数可以是虚函数,当覆写它们时,必须指定 suspend 修饰符:

    1. interface Base {
    2. suspend fun foo()
    3. }
    4. class Derived: Base {
    5. override suspend fun foo() { …… }
    6. }

    这时就需要@RestrictsSuspension注解了。当一个接收者类或者接口R被标注时,所有可挂起扩展都需要代理R的成员或者其它扩展。由于扩展时不能互相无限代理(会导致程序终止),这就保障了所有挂起都是通过调用R的成员,这样库作者就能完全掌控挂起方式了。

    不过这样的场景不常见,它需要所有的挂起都通过库的特殊方式实现。比如,用下面的 函数实现生成器时,必须保证协程中所有的挂起都是通过调用yield()或者yieldAll()来实现。这就是为什么 被标注为 :

    可以参看Github 源码

    协程内部机制

    这里并不打算全盘解释协程内部的工作原理,而是给大家一个整体上的概念。

    协程完全时通过编译技术(并不需要 VM 或者 OS 方面的支持)实现,挂起时借由代码转换实现。基本上所有的挂起函数(当然是有些优化措施,但这里我们不会深入说明)都被转换为状态机。在挂起前,下一个状态会存储在编译器生成的与本地变量关联的类中。到恢复协程时,本地变量会被恢复为挂起之前的状态。

    挂起的协程可以存储以及作为一个对象进行传递,该协程会继续持有其状态和本地变量。这样的对象的类型时Continuation,代码转换的整体实现思路是基于经典的 Continuation-passing style 。所有挂起函数要有一个额外的参数类型Continuation

    更多的细节可以参看 。其它语言(比如C# ECMASript2016)中类似的 async/await 模型在这里都有描述,当然了其它语言的实现机制和 Kotlin 有所不同

    协程的设计是实验性的,也就是说在后面的 releasees 版本中可能会有所变更。当在 Kotlin1.1 中编译协程时,默认会有警告:The feature “coroutines” is experimental 。可以通过 来移除警告。

    由于处于实验状态,协程相关的标准库都在kotlin.coroutines.experimental包下。当设计确定时实验状态将会取消,最后的API将会移到 kotlin.coroutines,实验性的包将会保留(或许是作为一个单独的构建中)以保持兼容。

    千万注意: 建议库作者可以采用同样的转换:为基于协程的 API 采用 “experimental” 前缀作包名(比如com.example.experimental)。当最终 API 发布时,遵循下面的步骤:

    • 复制所有 API 到 com.example包下

    这样可以减少用户的迁移问题。

    标准 API

    • 语言层面的支持(比如支持函数挂起)
    • Kotlin 标准库中核心底层 API
    • 可以直接在代码中使用的高级 API

    底层 API:kotlin.coroutines

    底层 API 比较少,强烈建议不要使用,除非要创建高级库。这部分 API 主要在两个包中:

    关于这些 API 用法的更多细节可以在这里找到。

    kotlin.coroutines中的生成器API:

    kotlin.coroutines.experimental 中唯一的“应用层面”的函数是:

    这些和 kotlin-stdlib 打包在一起,因为和序列相关。事实上,这些函数(这里单独以 buildSequence() 作为事例)实现生成器提供了一种更加简单的构造延迟序列的方法:

    1. val fibonacciSeq = buildSequence {
    2. var a = 0
    3. var b = 1
    4. yield(1)
    5. while (true) {
    6. yield(a + b)
    7. val tmp = a + b
    8. a = b
    9. b = tmp
    10. }
    11. }

    这里通过调用 yield()函数生成新的斐波那契数,就可以生成一个无限的斐波那契数列。当遍历这样的数列时,每遍历一步就生成一个斐波那契数,这样就可以从中取出无限的斐波那契数。比如 fibonacciSeq.take(8).toList()会返回[1, 1, 2, 3, 5, 8, 13, 21]。协程让这一实现开销更低。

    为了演示正真的延迟序列,在buildSequence()中打印一些调试信息:

    1. print("START ")
    2. yield(i)
    3. print("STEP ")
    4. }
    5. print("END")
    6. }
    7. // Print the first three elements of the sequence
    8. lazySeq.take(3).forEach { print("$it ") }

    运行上面的代码运,如果我们输出前三个元素的数字与生成循环的 STEP 有交叉。这意味着计算确实是惰性的。要输出 1,我们只执行到第一个 yield(i),并且过程中会输出 START。然后,输出 2,我们需要继续下一个 yield(i),并会输出 STEP3 也一样。永远不会输出再下一个 STEP(以及END),因为我们没有请求序列的后续元素。

    使用 yieldAll() 函数可以一次性生成序列所有值:

    buildIterator()buildSequence()作用相似,只不过返回值时延迟迭代器。

    通过给SequenceBuilder类写挂起扩展,可以给 buildSequence()添加自定义生成逻辑:

    1. suspend fun SequenceBuilder<Int>.yieldIfOdd(x: Int) {
    2. if (x % 2 != 0) yield(x)
    3. }
    4. val lazySeq = buildSequence {
    5. for (i in 1..10) yieldIfOdd(i)
    6. }

    其它高级API:kotlinx.coroutines

    Kotlin 标准库只提供与协程相关的核心 API 。主要有基于协程的库核心原语和接口可以使用。

    • 平台无关的异步编程此模块kotlinx-coroutines-core
      • 包括类似 Go 语言的select 和其他便利原语
      • 这个库的综合指南查看。
    • 基于 JDK 8 中的 CompletableFuture 的 API:kotlinx-coroutines-jdk8
    • 基于 JDK 7 及更高版本 API 的非阻塞 IO(NIO):kotlinx-coroutines-nio
    • 支持 Swing (kotlinx-coroutines-swing) 和 JavaFx (kotlinx-coroutines-javafx)
    • 支持 RxJava:kotlinx-coroutines-rx

    这些库既提供了方便的 API ,也可以作为构建其它基于协程库的样板参考。