Kotlin协程设计方案(Kotlin Coroutines Design Proposal 中文版)

(原文:https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md)

  • 类型:设计方案
  • 作者: Andrey Breslav, Roman Elizarov
  • 贡献者: Vladimir Reshetnikov, Stanislav Erokhin, Ilya Ryzhenkov, Denis Zharkov
  • 状态:自 Kotlin 1.3(修订版 3.3)起稳定,在 Kotlin 1.1-1.2 中处于试验阶段。

摘要


这是对 Kotlin 协程的描述。 这个概念也被称为,或者部分涵盖:

  • generators/yield
  • async/await
  • 可组合的/定界的 сontinuations

目标:

  • 不依赖 Futures 或其他此类丰富的库的特定实现;
  • 同等涵盖 “async/await” 用例和 “generator blocks”;
  • 可以使用 Kotlin 协程作为已存在的不同 的异步 API(如 Java NIO、Futures 的不同实现等)的包装器。

目录


  • 用例
    • 异步计算
    • Futures
    • 生成器(Generators)
    • 异步 UI
    • 更多用例
  • 协程概述
    • 术语
    • Continuation 接口
    • 挂起函数(Suspending functions)
    • 协程构建器(Coroutine builders)
    • 协程上下文(Coroutine context)
    • 延续拦截器(Continuation interceptor)
    • 受限挂起(Restricted suspension)
  • 实现细节
    • 延续传递风格(Continuation passing style)
    • 状态机
    • 编译挂起函数
    • 协程内在函数(Coroutine intrinsics)
  • 附录
    • 资源管理和 GC
    • 并发和线程
    • 异步编程风格
    • 包装回调(Wrapping callbacks)
    • 构建 future
    • 非阻塞睡眠(Non-blocking sleep)
    • 协作式单线程多任务
    • 异步序列(Asynchronous sequences)
    • 通道(Channels)
    • 互斥锁(Mutexes)
    • 从实验协程迁移
    • 参考

用例


协程可以被认为是一个可挂起计算的实例,即可以在某些地方挂起,然后在另一个线程上恢复执行。 协程的相互调用(和来回传递数据)可以形成多任务处理协作的机制。

异步计算

协程用例的首要目的是异步计算(在 C# 和其他语言中是由 async/await 处理)。 让我们看看如何使用回调完成此类计算。 让我们以异步 I/O 为例(以下 API 已简化)作为启发:

// 异步读入到 buf,当完成时运行下面的 lambda
inChannel.read(buf) {
    // 当读数据完成时,这个 lambda 被执行
    bytesRead ->
    ...
    ...
    process(buf, bytesRead)
    
    // 异步将 buf 写入,当完成时运行下面的 lambda
    outChannel.write(buf) {
        // 当写数据完成时,这个 lambda 被执行
        ...
        ...
        outFile.close()          
    }
}

需要注意的是,在一个回调中有另外一个回调,尽管可以使我们免于使用大量模板代码(例如,只是将 buf 参数视为闭包的一部分,而无需显式传递给回调),但是代码缩进级别每次都在增长,在嵌套级别大于 1 时很容易就会出现此问题(搜索“回调地狱 callback hell ”可以看到有多少人在 JavaScript 中遇到此问题)。

同样的计算可以使用协程直接表示(前提是有一个库可以使 I/O API 适应协程要求):

launch {
    // 当异步读数据的时候挂起
    val bytesRead = inChannel.aRead(buf) 
    // 只有在读数据完成的时候,才能到达这一行
    ...
    ...
    process(buf, bytesRead)
    // 当异步写数据的时候挂起  
    outChannel.aWrite(buf)
    // 只有在写数据完成的时候,才能到达这一行 
    ...
    ...
    outFile.close()
}

这里的 aRead()aWrite() 是特殊的 挂起函数(suspending functions) ——它们可以 挂起(suspend) 执行(这并不意味着会去阻塞它一直在运行的线程),并在调用完成时 恢复(resume) 。我们可以想象一下,将 aRead() 之后的所有代码都被包装在一个 lambda 中,并作为回调传递给 aRead(),对于 aWrite() 也同样如此操作,那我们可以看到这样的代码和上面的示例代码一样,只是更具可读性。

我们的明确目标就是以非常通用的方式支持协程,所以在这个例子中, launch{}.aRead().aWrite() 只是适用于使用协程的 库函数(library functions)launch协程构建器(coroutine builder) — 用于构建和启动协程,而 aRead/aWrite 是特殊的 挂起函数(suspending functions) 隐式接收
计算延续(continuations)(延续只是通用回调)。

launch{} 的示例代码在 协程构建器 章节中呈现,并且.aRead() 的示例代码在 包装回调 章节中呈现。

需要注意的是,在循环中间以异步调用方式显式传递回调会很棘手,但在协程中,却是一件很正常的事情:

launch {
    while (true) {
        // 异步读数据的时候挂起
        val bytesRead = inFile.aRead(buf)
        // 当读数据完成时继续
        if (bytesRead == -1) break
        ...
        process(buf, bytesRead)
        // 异步写数据的时候挂起
        outFile.aWrite(buf) 
        // 当写数据完成时继续
        ...
    }
}

可以想到,在协程中处理异常也会更方便一些。

Futures

Futures(也称为 Promises 或 Deferreds)是另一种表示异步计算的方式。 在这里我们将使用一个虚构的 API,将一个叠加层应用于一张图像上:

val future = runAfterBoth(
    loadImageAsync("...original..."), // 创建一个 Future 
    loadImageAsync("...overlay...")   // 创建一个 Future
) {
    original, overlay ->
    ...
    applyOverlay(original, overlay)
}

使用协程,就可以重写为如下代码:

val future = future {
    val original = loadImageAsync("...original...") // 创建一个 Future
    val overlay = loadImageAsync("...overlay...")   // 创建一个 Future
    ...
    // 当等待图片的加载时挂起,然后当图片都加载完成时,运行 `applyOverlay(...)` 
    applyOverlay(original.await(), overlay.await())
}

future{} 的示例代码在 构建 futures 章节展示,.await() 的示例代码在 挂起函数(suspending-functions) 章节展示.

同样,使用 futures ,可以得到更少的缩进和更自然的组合逻辑(以及此处未展示的异常处理),并且没有特殊的关键字(如 C#、JS 和其他语言中的 asyncawait),future{}.await() 只是库中的函数。

生成器(Generators)

延迟计算的序列是协程的另一个典型用例(在 C#、Python 和许多其他语言中由 yield 处理)。通过看似顺序的代码生成的一个序列,在运行时只需要计算请求的元素:

// 推断的类型是 Sequence
val fibonacci = sequence {
    yield(1) // 第一个斐波那契数
    var cur = 1
    var next = 1
    while (true) {
        yield(next) // 下一个斐波那契数
        val tmp = cur + next
        cur = next
        next = tmp
    }
}

这段代码创建了一个 Fibonacci numbers 的 lazy Sequence,它可能是无限的(就像 Haskell 的无限列表)。我们可以请求序列中的一些,例如,通过 take()

println(fibonacci.take(10).joinToString())

这将打印 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 你可以试试在 这里 的代码

生成器的优势在于支持任意控制流,例如 while(来自上面的示例)、iftry/catch/finally 和其他所有内容:

val seq = sequence {
    yield(firstItem) // 挂起点

    for (item in input) {
        if (!item.isValid()) break // don't generate any more items
        val foo = item.toFoo()
        if (!foo.isGood()) continue
        yield(foo) // 挂起点    
    }
    
    try {
        yield(lastItem()) // 挂起点
    }
    finally {
        // some finalization code
    }
} 

sequence{}yield() 的示例代码在 受限挂起 章节展示。

请注意,还允许将 yieldAll(sequence) 表示为库函数( sequence{}yield() 也是),这简化了加入惰性序列(lazy sequences)和允许有效的实现。

异步 UI

典型的 UI 应用程序有一个事件调度线程,所有 UI 操作都在其中发生。通常不允许从其他线程修改 UI 状态。所有 UI 库都提供某种原语将执行移回 UI 线程。例如,Swing 有 SwingUtilities.invokeLater,JavaFX 有 Platform.runLater,Android 有 Activity.runOnUiThread 等。这是来自典型 Swing 应用程序的代码片段,执行一些异步操作,然后在 UI 中显示其结果:

makeAsyncRequest {
    // 当异步请求完成时,这个 lambda 被执行 
    result, exception ->
    
    if (exception == null) {
        // 在 UI 中显示结果 result
        SwingUtilities.invokeLater {
            display(result)   
        }
    } else {
       // 处理异常 exception
    }
}

这类似于我们在 异步计算 用例中看到的回调地狱,它也可以通过协程优雅地解决:

launch(Swing) {
    try {
        // 当异步发出请求时挂起
        val result = makeRequest()
        // 在 UI 线程中显示结果, 这里 Swing 上下文确保我们始终停留在事件派发线程中
        display(result)
    } catch (exception: Throwable) {
        // 处理异常
    }
}

Swing 上下文的示例代码在 延续拦截器 章节中展示。

所有异常处理都是使用自然语言结构(natural language constructs)执行的。

更多用例

协程可以涵盖更多用例,包括:

  • 基于 channel 的并发(又名 goroutine 和 channels);
  • 基于 actor 的并发;
  • 偶尔需要与用户交互的后台进程,例如,显示模态对话框;
  • 通信协议:将每个 actor 实现为序列而不是状态机;
  • Web 应用程序工作流程:注册用户、验证电子邮件、登录(被挂起的协程可能会被序列化并存储在数据库中)。

协程概述


本节概述了支持编写协程的语言机制以及管理其语义的标准库。

术语

  • 协程(coroutine) —— 是一个 可挂起计算实例。它在概念上类似于线程,因为它需要一段代码来运行,并且具有类似的生命周期 —— createdstarted,但它不绑定到任何特定线程。它可以在一个线程中 suspend 执行,并在另一个线程中 resume。此外,就像 future 或 promise,它可能会 完成得到 一些结果(一个值或一个异常)。

  • 挂起函数(suspending function) —— 一个用 suspend 修饰符标记的函数。它可以通过调用其它挂起函数来 挂起 执行代码而不阻塞当前的执行线程。挂起函数不能从常规代码处调用,而只能从其它挂起函数和挂起 lambdas(suspending lambdas)处 调用(见下文)。例如,如 用例 所示,.await()yield(),是在库中定义的挂起函数。标准库提供了用于定义所有其他挂起函数的原始挂起函数。

  • 挂起 lambda(suspending lambda) —— 必须在协程中运行的代码块。它看起来完全像一个普通的 lambda 表达式,但是用 suspend 修饰符标记了它的函数类型。就像常规 lambda 表达式是匿名局部函数的短句法形式一样,挂起 lambda 是匿名挂起函数的短句法形式。它可以通过调用挂起函数来 挂起 执行代码而不阻塞当前的执行线程。例如,在 用例 中显示的 launchfuturesequence 函数后面的花括号中的代码块正是挂起 lambda。

    注意:常规 lambda 可以在其代码的所有位置调用挂起函数,包括在此 lambda 的 非局部 return 语句处。也就是说,允许在内联 lambda 中(如apply{} 块 ),调用挂起函数,但不允许在 noinline 或是 crossinline 的内部 lambda 表达式中调用。 挂起(suspension) 被视为一种特殊的非局部控制转移(non-local control transfer)。

  • 挂起函数类型(suspending function type) —— 是挂起函数和挂起 lambda 的函数类型。就像常规 函数类型,但带有 suspend 修饰符。例如,suspend () -> Int 是一个无参、返回 Int的挂起函数类型。声明为 suspend fun foo(): Int 的挂起函数符合此函数类型。

  • 协程构建器(coroutine builder) —— 是一个函数,它接受一些 挂起 lambda 作为参数,创建一个协程,并且可选的,能够以某种形式访问其结果。例如,用例 中所示的 launch{}future{}sequence{} 都是协程构建器。标准库提供了原始协程构建器,用于定义所有其他协程构建器。

    注意:某些语言是硬编码支持以特定方式创建和启动协程的,这个协程定义了如何表示这些语言的执行和结果。例如,generate 关键字 可以定义一个返回某种可迭代对象的协程,而 async 关键字 可以定义一个返回某种 promise 或 task 的协程。 Kotlin 没有关键字或修饰符来定义和启动协程。协程构建器只是定义在库中的函数。如果在其它语言中协程定义采用方法体的形式,那么在 Kotlin 中,协程定义方法通常是带有表达式体的常规方法,是由一些在库中定义的、最后一个参数是挂起 lambda的协程构建器的调用组成 :

    fun doSomethingAsync() = async { ... }
    
  • 挂起点(suspension point) —— 是在协程执行期间,协程的执行 可能会被挂起 的时刻。从语法上讲,挂起点是对挂起函数的调用,但 实际 挂起发生在挂起函数调用标准库原语以挂起执行时。

  • 延续(continuation) —— 是在挂起点被挂起的协程的状态。它在概念上表示为在挂起点之后的其余执行。例如:

    sequence {
        for (i in 1..10) yield(i * i)
        println("over")
    }  
    

    在这里,每次协程在调用挂起函数 yield() 时被挂起,其执行的其余部分 都表示为一个 continuation,所以我们有 10 个 continuation:第一次运行循环 i = 1 ,并挂起, 第二次运行循环 i = 2,并挂起,最后一次循环,打印“over”,完成协程。已创建但尚未启动的协程由其类型为 Continuation初始延续(initial continuation) 表示,该 continuation 构成了整个执行。

    (注:原文文字解释第一次循环是 i = 2,第二次循环是 i = 3,但对照上面的代码,应该是从 i = 1 开始循环。)

如上所述,协同程序的驱动要求之一是灵活性:我们希望能够支持许多现有的异步 API 和其他用例,并尽量减少硬编码加入到编译器中。 因此,编译器唯一需要负责的是支持挂起函数、挂起 lambda 以及相应的挂起函数类型。 标准库中的原语很少,其余的留给应用程序库。

Continuation 接口

这是接口 Continuation 在标准库中的定义(在kotlin.coroutines包中定义),它表示一个通用回调:

interface Continuation<in T> {
   val context: CoroutineContext
   fun resumeWith(result: Result<T>)
}

context 在 协程上下文 章节中有详细介绍,它表示与协程关联的任意的用户定义的 context。 resumeWith 函数是一个表示 完成 的回调,用于在协程完成时报告成功(带有参数 value)或失败(带有参数 exception)。

为方便起见,在同一个包中定义了两个扩展函数:

fun <T> Continuation<T>.resume(value: T)
fun <T> Continuation<T>.resumeWithException(exception: Throwable)

挂起函数(Suspending functions)

.await()这样的典型 挂起函数 的实现如下所示:

suspend fun <T> CompletableFuture<T>.await(): T =
    suspendCoroutine<T> { cont: Continuation<T> ->
        whenComplete { result, exception ->
            if (exception == null) // the future has been completed normally
                cont.resume(result)
            else // the future has completed with an exception
                cont.resumeWithException(exception)
        }
    }

你可以在 这里 获取此代码。注意:如果 future 永远不会完成,这个简单的实现会永远挂起协程。 kotlinx.coroutines 中的真实的实现支持取消。

suspend 修饰符表示这是一个可以挂起协程的执行的函数。这个特定的函数被定义为一个 CompletableFuture 类型的扩展函数 ,因此它函数的使用自然地按照从左到右的顺序读取,这个顺序与实际执行顺序相对应:

doSomethingAsync(...).await()

修饰符 suspend 可用于任何函数:顶级函数、扩展函数、成员函数、局部函数或运算符函数。

属性 getter 和 setter、构造函数和一些操作符函数(即 getValuesetValueprovideDelegategetsetequals)不能有 suspend 修饰符。这些限制将来可能会取消。

挂起函数可以调用任何常规函数,但要真正挂起执行,它们必须调用其他的挂起函数。特别是,这个 await 实现调用了标准库中(在 kotlin.coroutines 包中)定义的挂起函数 suspendCoroutine 作为顶级挂起函数:

suspend fun <T> suspendCoroutine(block: (Continuation<T>) -> Unit): T

suspendCoroutine 在协程内部被调用时(它 只能 在协程内部调用,因为它是一个挂起函数),它会在 Continuation 实例中捕获协程的执行状态,并将此 continuation 传递给指定的参数 block。为了恢复协程的执行,这个 block 稍后会在此线程或在其他线程中调用 continuation.resumeWith()(直接调用或使用 continuation.resume()continuation.resumeWithException() 扩展)。协程的 真实的 挂起发生在没有调用 resumeWith 的情况下, suspendCoroutine 块返回时。如果协程在从块内部返回之前 resume 了,则这个协程就不被认为已经挂起,并且会继续执行。

结果 result 传递给 continuation.resumeWith() 作为对 suspendCoroutine 调用的结果, 而 suspendCoroutine 调用的结果又依次成为 .await() 的结果。

不允许对相同的continuation进行多次恢复,这会产生 IllegalStateException

注意:这是 Kotlin 中的协程与函数式语言(如 Scheme或 Haskell 中的 continuation monad )中的一等定界延续(first-class delimited continuations)之间的主要区别。只恢复一次 continuation (resume-once continuations)的选择是纯粹注重实效的,因为没有一个专门的 用例 是需要多次恢复 continuation ( multi-shot continuations)的。但是,多次恢复的 continuation(multi-shot continuations),可以作为一个单独的库被实现,方式是通过使用低级 协程内在函数 挂起协程并且克隆在 continuation 中捕获的协程状态,这样克隆的协程状态(也就是 continuation)就可以再一次被恢复。

协程构建器(Coroutine builders)

挂起函数不能从常规函数处调用,因此标准库提供了函数,从常规非挂起范围内启动协程执行。下面是一个简单的 协程构建器 launch{} 的实现:

fun launch(context: CoroutineContext = EmptyCoroutineContext, block: suspend () -> Unit) =
    block.startCoroutine(Continuation(context) { result ->
        result.onFailure { exception ->
            val currentThread = Thread.currentThread()
            currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
        }
    })

你可以在 这里 获取此代码。

此实现使用函数 Continuation(context) { ... } (来自 kotlin.coroutines包),该函数提供了一个快捷方式来实现一个 Continuation 接口,带有给定的值:它的contextresumeWith 函数体。此 continuation 作为 completion continuation 传递给扩展函数 block.startCoroutine(...) (来自 kotlin .coroutines 包)。

协程完成会调用它的 completion continuation。在协程 完成 成功或失败时,continuation 的 resumeWith 函数会被调用。因为 launch 启动了协程后就不再管理该协程,它的挂起函数具有 Unit 返回类型,在 resume 函数中忽略这个 Unit 类型的返回结果。如果协程执行完成异常,则使用当前线程的 uncaught exception handler 进行报告。

注意:这个简单的实现返回 Unit 并且根本不提供对协程状态的访问。 kotlinx.coroutines 中的实际实现更复杂,因为它返回一个代表协程的Job 接口的实例,并且可以取消。

context 在 协程上下文 部分中有详细介绍。 startCoroutine 在标准库中被定义为无参数和单参数挂起函数类型的扩展:

fun <T> (suspend  () -> T).startCoroutine(completion: Continuation<T>)
fun <R, T> (suspend  R.() -> T).startCoroutine(receiver: R, completion: Continuation<T>)

startCoroutine 创建协程并立即在当前线程中开始执行(但请参阅下面的备注),直到第一个 挂起点,然后它返回。挂起点是在协程主体中的一些挂起函数的调用,并且是由相应的挂起函数的代码来定义何时以及如何恢复协程执行。

注意:稍后章节 涉及的延续拦截器(continuation interceptor)(来自 context)可以调度协程的执行(包括 它的初始延续(initial continuation))到另一个线程。

协程上下文(Coroutine context)

协程 context 是一组不可变的用户定义对象,可以被添加到协程上。这些对象可以负责协程线程策略、日志记录、协程执行的安全和事务方面、协程标识和名称等。下面协程及其 context 的简单模型。设想将协程视为轻量级线程。在这种情况下,协程 context 就像线程局部变量的集合。不同之处在于线程局部变量是可变的,而协程 context 是不可变的,这对协程来说并不是一个严重的限制,因为协程是如此轻量级,以至于在上下文中需要更改任何内容时可以很容易启动新的协程。

标准库不包含 context 元素的任何具体实现,但具有接口和抽象类,以便所有的这些都可以在库中以 可组合 的方式定义,以便来自不同库的这些可以作为同一个 context 的元素和平共存.

从概念上讲,协程 context 是一组索引元素,其中每个元素都有一个唯一的键。它是 set 和 map 的混合。它的元素像 map 一样有 key,但它的 key 直接与元素相关联,更像是 set。标准库为 CoroutineContext 定义了的最小接口(在kotlin.coroutines包中):

interface CoroutineContext {
    operator fun <E : Element> get(key: Key<E>): E?
    fun <R> fold(initial: R, operation: (R, Element) -> R): R
    operator fun plus(context: CoroutineContext): CoroutineContext
    fun minusKey(key: Key<*>): CoroutineContext

    interface Element : CoroutineContext {
        val key: Key<*>
    }

    interface Key<E : Element>
}

CoroutineContext 本身有四个可用的核心操作:

  • 运算符 get :对于给定的 key,提供对元素的类型安全的访问。如 Kotlin 运算符重载 中所述,它可以与符号 [..] 一起使用。

  • 函数 fold 的作用类似于在标准库中扩展的 Collection.fold ,并提供了在 context 中迭代(iterate)所有元素的手段。

  • 运算符 plus 的作用类似于在标准库中扩展的 Set.plus ,并返回两个 context 的组合,位于 plus 右侧的 context 元素,会替换位于 plus 左侧的拥有相同 key 的 context 元素。

  • 函数 minusKey 返回一个不包含指定 key 的 context。

协程 context 的 元素(Element) 本身是一个 context。它是仅包含此元素的单例 context。这可以通过获取协程 context 元素的库定义并将它们与 + 连接来创建复合 context。例如,如果一个库用用户授权信息定义了 auth 元素,而另一个库用一些执行 context 信息定义了 threadPool 对象,那么您可以使用 launch{} 协程构建器 与使用 launch(auth + threadPool) {...} 调用的复合 context。

注意:kotlinx.coroutines 提供了几个 context 元素,包括 Dispatchers.Default 对象,它调度协程的执行到后台线程的共享池中。

标准库提供 EmptyCoroutineContext —— 没有任何元素的 CoroutineContext 实例(空)。

所有第三方 context 元素都应扩展 AbstractCoroutineContextElement 类,该类由标准库提供(在 kotlin.coroutines 包中)。对于库定义的 context 元素,建议使用以下样式。下面的示例显示了一个假设的授权 context 元素,该元素存储了当前用户名:

class AuthUser(val name: String) : AbstractCoroutineContextElement(AuthUser) {
    companion object Key : CoroutineContext.Key<AuthUser>
}

这个例子可以在 这里 找到。

将 context Key 定义为相应元素类的伴生对象,可以流畅地访问 context 的相应元素。这是一个需要检查当前用户名的挂起函数的假设实现:

suspend fun doSomething() {
    val currentUser = coroutineContext[AuthUser]?.name ?: throw SecurityException("unauthorized")
    // do something user-specific
}

它使用顶级 coroutineContext 属性(来自 kotlin.coroutines 包),可以在挂起函数中检索当前协程的 context。

延续拦截器(Continuation interceptor)

让我们回顾一下 异步 UI 用例。尽管各种挂起函数会在任意线程中恢复协程执行,但异步 UI 应用程序必须确保协程主体本身始终在 UI 线程中执行。这是使用 continuation 拦截器 完成的。首先,我们需要充分了解协程的生命周期。看一段使用协程构建器 launch{} 的代码片段:

launch(Swing) {
    initialCode() // 初始化代码执行
    f1.await() // 挂起点 #1
    block1() // 执行 #1
    f2.await() // 挂起点 #2
    block2() // 执行 #2
}

协程从执行 initialCode 开始,直到第一个挂起点。在挂起点 挂起,并在一段时间后,根据相应的挂起函数的定义,恢复 执行 block1,然后再次挂起和恢复执行block2,之后 完成

Continuation 拦截器可以选择拦截和包装对应于 initialCodeblock1block2 从恢复到后续挂起点的执行的 continuation。协程的初始代码被视为其 initial continuation 的恢复。标准库提供了接口 ContinuationInterceptor(在kotlin.coroutines包中) :

interface ContinuationInterceptor : CoroutineContext.Element {
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>
    fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
    fun releaseInterceptedContinuation(continuation: Continuation<*>)
}

interceptContinuation 函数包装了协程的continuation。每当协程挂起时,协程框架使用以下代码行来包装实际的 continuation 以供随后恢复:

val intercepted = continuation.context[ContinuationInterceptor]?.interceptContinuation(continuation) ?: continuation

对于每个实际的 continuation 实例,协程框架会缓存最终被拦截的 continuation,并在不再需要的时候,调用releaseInterceptedContinuation(intercepted)。有关更多详细信息,请参阅 实现细节 章节。

请注意,像 await 这样的挂起函数实际上可能会也可能不会挂起协程的执行。例如,在 挂起函数 章节中展示的 await 实现,当 future 已经完成时,并没有真正挂起协程(在这种情况下,它立即调用 resume 并继续执行而没有真正的挂起)。只有在协程执行期间发生真正的挂起时 continuation 才会被拦截,即当不调用 resume而返回 suspendCoroutine 块时。

让我们看一下Swing 拦截器的具体示例代码,它分派执行到 Swing UI 事件分派线程。我们从定义一个 SwingContinuation 包装类开始,它使用 SwingUtilities.invokeLater 分派 continuation 到 Swing 事件分派线程:

private class SwingContinuation<T>(val cont: Continuation<T>) : Continuation<T> {
    override val context: CoroutineContext = cont.context
    
    override fun resumeWith(result: Result<T>) {
        SwingUtilities.invokeLater { cont.resumeWith(result) }
    }
}

然后定义 Swing 对象作为相应的 context 元素并实现 ContinuationInterceptor 接口:

object Swing : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
        SwingContinuation(continuation)
}

你可以在 这里 获取此代码。注意:在 kotlinx.coroutines 中 Swing 对象的实际实现也支持协程调试工具,提供并显示在当前正在运行此协程的线程的名称中当前正在运行的协程的标识符。

现在,可以使用带有 Swing 参数的 协程构建器 launch{}来执行一个完全运行在 Swing 事件调度线程中的协程:

launch(Swing) {
  // 这里的代码可以挂起,但将总是在 Swing EDT 中恢复
}

kotlinx.coroutines 中的 Swing context 的实际实现更加复杂,因为它集成了库里的时间和调试工具。

受限挂起(Restricted suspension)

需要一种不同类型的协程构建器和挂起函数来实现 生成器 用例中的 sequence{}yield()。以下是 sequence{} 协程构建器的示例代码:

fun <T> sequence(block: suspend SequenceScope<T>.() -> Unit): Sequence<T> = Sequence {
    SequenceCoroutine<T>().apply {
        nextStep = block.createCoroutine(receiver = this, completion = this)
    }
}

它使用与标准库不同的原语,称为 createCoroutine,它类似于 startCoroutine(即在 协程构建器 章节中进行了解释)。然而,它 创建 了一个协程,但 没有 启动它。相反,它返回其 initial continuation 作为对 Continuation 的引用:

fun <T> (suspend () -> T).createCoroutine(completion: Continuation<T>): Continuation<Unit>
fun <R, T> (suspend R.() -> T).createCoroutine(receiver: R, completion: Continuation<T>): Continuation<Unit>

另一个区别是此构建器的 挂起 lambda 是带有 SequenceScope receiver 的 扩展 lambdaSequenceScope 接口为生成器模块提供了 scope 并在库中定义为:

interface SequenceScope<in T> {
    suspend fun yield(value: T)
}

为了避免创建多个对象,sequence{} 的实现定义了 SequenceCoroutine 类,该类实现了 SequenceScopeContinuation,因此它可以同时作为用于 createCoroutinereceiver 参数和它的 completion Continuation 参数。 SequenceCoroutine 的简单实现如下所示:

private class SequenceCoroutine<T>: AbstractIterator<T>(), SequenceScope<T>, Continuation<Unit> {
    lateinit var nextStep: Continuation<Unit>

    // AbstractIterator implementation
    override fun computeNext() { nextStep.resume(Unit) }

    // Completion continuation implementation
    override val context: CoroutineContext get() = EmptyCoroutineContext

    override fun resumeWith(result: Result<Unit>) {
        result.getOrThrow() // bail out on error
        done()
    }

    // Generator implementation
    override suspend fun yield(value: T) {
        setNext(value)
        return suspendCoroutine { cont -> nextStep = cont }
    }
}

你可以在 这里 获取此代码。请注意,标准库提供了此 sequence 函数的开箱即用优化实现(在 kotlin.sequences 包中),额外支持 yieldAll 函数.

sequence 的实际代码使用实验性的 BuilderInference 功能,如 生成器 章节所示,该功能可以声明 fibonacci,无需显式指定序列类型参数 T。相反,它是从传递给 yield 调用的类型推断出来的。

yield 的实现使用挂起函数 suspendCoroutine 来挂起协程并捕获它的 continuation。Continuation 存储为 nextStep,用来在调用 computeNext 时被恢复。

但是,如上所示,sequence{}yield() 还没有准备好让任意挂起函数在其范围内捕获 continuation。它们是 同步 工作。他们需要对如何捕获 continuation、存储位置以及何时恢复进行绝对控制。它们形成了 受限的挂起范围(restricted suspension scope)。受限挂起的能力由放置在类或接口作用域的 @RestrictsSuspension 注释提供,在上面的示例中,此作用域接口是 SequenceScope

@RestrictsSuspension
interface SequenceScope<in T> {
    suspend fun yield(value: T)
}

此注释可对在 sequence{} 或类似同步协程构建器范围内使用的挂起函数实施某些特定限制。任何扩展挂起 lambda 或函数,它以 受限挂起作用域 的类或接口(被 @RestrictsSuspension 标记)作为其 receiver,则称为 受限挂起函数。受限挂起函数只能在其受限挂起作用域内的同一实例上调用成员或扩展挂起函数。

特别是,这意味着没有任何在 lambda 作用范围内的 它 的 SequenceScope 扩展可以调用 suspendContinuation 或其他通用挂起函数。要挂起 sequence 协程的执行,必须最终调用 SequenceScope.yieldyield 的实现本身是 SequenceScope 实现的成员函数,没有任何限制(只有 扩展 挂起 lambda 和函数是受限的)。

让像 sequence 这样的受限协程构建器支持任意 context 没有什么意义,因为它们的作用域类或接口(本例中的SequenceScope)就被用作了 context,因此受限协程必须始终使用 EmptyCoroutineContext 作为它们的 context,这就是 SequenceCoroutinecontext 属性的 getter 的实现返回的内容。尝试使用非 EmptyCoroutinesContext 的 context 创建受限协程会导致 IllegalArgumentException

实现细节


本节简要介绍协程的实现细节。它们隐藏在 协程概述 章节中解释的构建块的后面,只要它们不违反公开的 API 和 ABI 的协议,它们的内部类和代码生成策略随时可能发生变化。

延续传递风格(Continuation passing style)

挂起函数通过 Continuation-Passing-Style (CPS) 实现。每个挂起函数和挂起 lambda 都有一个附加的 Continuation 参数,当挂起函数和挂起 lambda 被调用时会隐式传递Continuation 参数进来。回想一下,await 挂起函数 的声明如下所示:

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

然而,它的实际 实现 在进行 CPS转换 之后具有以下签名:

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

它的结果类型 T 已移至其附加 continuation 参数中的类型参数的位置。 结果类型 Any? 的实现旨在表示挂起函数的动作。当挂起函数 挂起 协程时,它​​会返回一个特殊的标记值 COROUTINE_SUSPENDED(在 协程内在函数 章节中查看更多信息)。当挂起函数没有挂起当前协程而是继续协程的执行时,它会返回其结果或直接抛出异常。这样,await 实现的返回类型 Any? 实际上是 COROUTINE_SUSPENDEDT 的联合,在 Kotlin 的类型系统中无法表达。

挂起函数的实际实现是不允许直接在其栈帧中调用 continuation 的,因为这可能导致长时间运行的协程的堆栈溢出。标准库中的 suspendCoroutine 函数通过追踪 continuation 的调用向应用程序开发人员隐藏了这种复杂性,并且,无论如何以及何时调用 continuation,都能确保符合挂起函数的实际实现协议。

状态机

高效实现协程至关重要,即创建尽可能少的类和对象。许多语言通过 状态机 实现它们,而 Kotlin 也是如此。Kotlin 中,这种方法会导致编译器为每个挂起 lambda 只创建一个类,该类可能在其主体中具有任意数量的挂起点。

主要思想:一个挂起函数被编译成一个状态机,其中状态对应于挂起点。例如:让我们以一个带有两个挂起点的挂起块为例:

val a = a()
val y = foo(a).await() // suspension point #1
b()
val z = bar(a, y).await() // suspension point #2
c(z)

此代码块有三种状态:

  • 初始(在任何挂起点之前)
  • 在第一个挂起点之后
  • 在第二个挂起点之后

每个状态都是该块的一个 continuation 的入口点(初始 continuation 从第一行继续)。

代码被编译成一个匿名类,该类有一个实现状态机的方法,一个保存状态机当前状态的字段,以及在状态之间共享的协程的局部变量的多个字段(也可能有协程的闭包的字段,但在这种情况下它是空的)。这是上面块的伪 Java 代码,它使用 continuation 传递样式(style)来调用挂起函数 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:
        // 在当前调用中,期待的结果是 `null`
        a = a()
        label = 1
        result = foo(a).await(this) // 'this' 作为 continuation 被传递
        if (result == COROUTINE_SUSPENDED) return // 如果 await 已经挂起执行,则 return
      L1:
        // 外部代码已经恢复了这个协程,传递了 .await() 的结果 
        y = (Y) result
        b()
        label = 2
        result = bar(a, y).await(this) // 'this' 作为 continuation 被传递
        if (result == COROUTINE_SUSPENDED) return // 如果 await 已经挂起执行,则 return
      L2:
        // 外部代码已经恢复了这个协程,传递了 .await() 的结果 
        Z z = (Z) result
        c(z)
        label = -1 // 不允许更多的步骤
        return
    }          
}    

请注意,这里有一个 goto 运算符和多个标签,因为该示例描述了在字节码中而不是在源代码中发生的事情,。

现在,当协程启动时,我们调用它的resumeWith() —— label0,然后我们跳转到L0,然后我们做一些工作,将label设置为下一个状态 —— 1,调用.await(),如果协程的执行被挂起则返回。当我们想继续执行时,我们再次调用 resumeWith(),现在它继续到 L1,做一些工作,将状态设置为 2,调用 .await() 并再次如果挂起就返回。下一次它从 L3 继续,将状态设置为 -1,这意味着“结束,没有更多工作要做”。

循环内的挂起点只生成一个状态,因为循环也通过(条件)goto 工作:

var x = 0
while (x < 10) {
    x += nextNumber().await()
}

生成为

class <anonymous_for_state_machine> extends SuspendLambda<...> {
    // The current state of the state machine
    int label = 0
    
    // local variables of the coroutine
    int x
    
    void resumeWith(Object result) {
        if (label == 0) goto L0
        if (label == 1) goto L1
        else throw IllegalStateException()
        
      L0:
        x = 0
      LOOP:
        if (x >= 10) goto END
        label = 1
        result = nextNumber().await(this) // 'this' is passed as a continuation 
        if (result == COROUTINE_SUSPENDED) return // return if await had suspended execution
      L1:
        // external code has resumed this coroutine passing the result of .await()
        x += ((Integer) result).intValue()
        label = -1
        goto LOOP
      END:
        label = -1 // No more steps are allowed
        return 
    }          
}    

编译挂起函数

挂起函数的编译代码取决于它调用其他挂起函数的方式和时间。在最简单的情况下,挂起函数仅在 尾部位置 对它们进行 尾调用 调用其他挂起函数。如 挂起函数 和 包装回调 章节所示,这是挂起函数实现低级同步原语或包装回调的典型情况。这些函数在尾部位置调用其他一些挂起函数,如 suspendCoroutine。它们像常规的非挂起函数一样编译,唯一的例外是它们从 CPS 转换 获得的隐式 continuation 参数被传递给尾调用中的下一个挂起函数。

如果挂起调用出现在非尾部位置,编译器会为相应的挂起函数创建一个 状态机。当挂起函数被调用时创建状态机对象实例,当挂起函数完成时丢弃该状态机对象实例。

注意:在未来的版本中,此编译策略可能会优化为仅在第一个挂起点创建状态机实例。

这个状态机对象,反过来,作为 completion continuation 用于在非尾部位置调用其他挂起函数。当函数多次调用其他挂起函数时,此状态机对象实例将被更新和重用。将此与其他 异步编程风格 进行比较,其中异步处理的每个后续步骤通常使用单独的、新分配的闭包对象来实现。

协程内在函数(Coroutine intrinsics)

Kotlin 标准库提供了 kotlin.coroutines.intrinsics 包,其中包含许多声明,这些声明公开了本节中解释的协程机制的内部实现细节,应谨慎使用。这些声明不应在一般代码中使用,因此 kotlin.coroutines.intrinsics 包在 IDE 中无法自动完成。为了使用这些声明,您必须手动地添加相应的 import 语句到源文件中:

import kotlin.coroutines.intrinsics.*

标准库中 suspendCoroutine 挂起函数的实际实现是用 Kotlin 编写的,其源代码可作为标准库源包的一部分获得。为了提供协程的安全和无问题使用,它将状态机的实际 continuation 包装到协程每次挂起时的附加对象中。这对于真正的异步用例(如 异步计算 和 futures)来说非常好,因为相应异步原语的运行时成本远远超过了额外分配对象的成本。但是,对于 生成器 用例,这种额外的成本是令人望而却步的,因此内在函数包为性能敏感的低级代码提供了原语。

标准库中的kotlin.coroutines.intrinsics包包含名为 suspendCoroutineUninterceptedOrReturn 的函数 具有以下签名:

suspend fun <T> suspendCoroutineUninterceptedOrReturn(block: (Continuation<T>) -> Any?): T

它提供了对挂起函数的 continuation 传递样式 的直接访问,并暴露了对 continuation 的 未被拦截的 引用。后者意味着调用 Continuation.resumeWith 不会通过 延续拦截器。在如下的情况时可以使用:用无法安装 continuation 拦截器(因为它们的 context 始终为空)的 受限挂起 编写同步协程;或者当前正在执行的线程已是所需的 context。否则,应通过扩展函数 intercepted (来自 kotlin.coroutines .intrinsics 包)获得被拦截的 continuation :

fun <T> Continuation<T>.intercepted(): Continuation<T>

并且 Continuation.resumeWith 应在产生的 被拦截的 continuation 上调用。

现在,传递给 suspendCoroutineUninterceptedOrReturn 函数的 block 可以返回 COROUTINE_SUSPENDED 标记协程是否挂起(在这种情况下 Continuation.resumeWith 应稍后调用一次)或返回结果值 T 或抛出异常(在最后两种情况下 Continuation.resumeWith 永远不会被调用) .

使用 suspendCoroutineUninterceptedOrReturn 时未能遵循此约定会导致难以跟踪错误,这些错误无法通过测试找到和重现它们。对于 buildSequence/yield 类协程,这种约定通常很容易遵循,但尝试在 suspendCoroutineUninterceptedOrReturn 之上编写异步 await 类似的挂起函数是不鼓励的,因为在没有 suspendCoroutine 的帮助下想要正确实现是非常棘手 的。

还有一些函数叫做 createCoroutineUnintercepted (来自kotlin.coroutines.intrinsics包)具有以下签名:

fun <T> (suspend () -> T).createCoroutineUnintercepted(completion: Continuation<T>): Continuation<Unit>
fun <R, T> (suspend R.() -> T).createCoroutineUnintercepted(receiver: R, completion: Continuation<T>): Continuation<Unit>

它们的工作方式类似于createCoroutine,但返回对初始延续的未拦截的引用。与 suspendCoroutineUninterceptedOrReturn 类似,它可以在同步协程中以获得更好的性能。例如,通过 createCoroutineUnintercepted 优化 sequence{} builder 的版本,如下所示:

fun <T> sequence(block: suspend SequenceScope<T>.() -> Unit): Sequence<T> = Sequence {
    SequenceCoroutine<T>().apply {
        nextStep = block.createCoroutineUnintercepted(receiver = this, completion = this)
    }
}

通过 suspendCoroutineUninterceptedOrReturn 优化 yield 版本如下所示。注意,因为 yield 总是挂起,相应的块总是返回 COROUTINE_SUSPENDED

// Generator implementation
override suspend fun yield(value: T) {
    setNext(value)
    return suspendCoroutineUninterceptedOrReturn { cont ->
        nextStep = cont
        COROUTINE_SUSPENDED
    }
}

你可以得到完整的代码 这里

两个额外的内在函数提供了 startCoroutine 的低级版本(请参阅 协程构建器 部分)并称为 startCoroutineUninterceptedOrReturn

fun <T> (suspend () -> T).startCoroutineUninterceptedOrReturn(completion: Continuation<T>): Any?
fun <R, T> (suspend R.() -> T).startCoroutineUninterceptedOrReturn(receiver: R, completion: Continuation<T>): Any?

它们在两个方面与 startCoroutine 不同。首先,启动协程时不会自动使用 延续拦截器,因此如果需要,调用者必须确保正确的执行上下文。第二,如果协程没有挂起,而是返回值或者抛出异常,那么调用startCoroutineUninterceptedOrReturn返回这个值或者抛出这个异常。如果协程挂起,则返回 COROUTINE_SUSPENDED

startCoroutineUninterceptedOrReturn 的主要用例是将它与 suspendCoroutineUninterceptedOrReturn 结合起来,以在相同的上下文中使用不同的代码块继续运行挂起的协程:

suspend fun doSomething() = suspendCoroutineUninterceptedOrReturn { cont ->
    // figure out or create a block of code that needs to be run
    startCoroutineUninterceptedOrReturn(completion = block) // return result to suspendCoroutineUninterceptedOrReturn 
}

附录


这是一个非规范性章节,不介绍任何新的语言结构或库函数,但涵盖了一些涉及资源管理、并发性和编程风格的附加主题,并为各种用例提供​​了更多示例。

资源管理和 GC

协程不使用任何堆外存储,并且它们本身不消耗任何本机资源,除非在协程中运行的代码确实打开了文件或其他资源。虽然在协程中打开的文件必须以某种方式关闭,但协程本身不需要关闭。当协程被挂起时,它的整个状态都可以通过对其延续的引用来获得。如果你失去了对挂起协程延续的引用,那么它最终将被垃圾收集器收集。

打开一些可关闭资源的协程需要特别关注。考虑以下协程,它使用 受限挂起 章节中的 sequence{} 构建器从文件中生成一系列行:

fun sequenceOfLines(fileName: String) = sequence<String> {
    BufferedReader(FileReader(fileName)).use {
        while (true) {
            yield(it.readLine() ?: break)
        }
    }
}

此函数返回一个 Sequence,您可以使用此函数以自然的方式打印文件中的所有行:

sequenceOfLines("https://github.com/kotlin/kotlin-coroutines-examples/tree/master/examples/sequence/sequenceOfLines.kt")
    .forEach(::println)

你可以得到完整的代码 这里

只要您完全迭代 sequenceOfLines 函数返回的序列,它就可以按预期工作。但是,如果您只打印此文件的前几行,如下所示:

sequenceOfLines("https://github.com/kotlin/kotlin-coroutines-examples/tree/master/examples/sequence/sequenceOfLines.kt")
        .take(3)
        .forEach(::println)

然后协程恢复几次以产生前三行并成为_被放弃的_。可以放弃协程本身,但不能放弃打开的文件。 函数 use 将没有机会完成其执行并关闭文件。该文件将保持打开状态,直到被 GC 收集,因为 Java 文件有一个关闭文件的 finalizer。对于小型幻灯片软件或短期运行的实用程序来说,这不是什么大问题,但对于具有数 GB 堆的大型后端系统来说,这可能是一场灾难,它用完打开文件句柄的速度可能比用完它的内存去触发GC的速度更快。

这是一个类似于 Java 的 Files.lines 方法产生一个惰性的行流。它返回一个可关闭的 Java 流,但大多数流操作不会自动调用相应的 Stream.close 方法,用户需要记住是否需要关闭相应的流。可以在 Kotlin 中定义可关闭的序列生成器,但它们会遇到类似的问题,即语言中没有自动机制可以确保它们在使用后关闭。为自动化资源管理引入语言机制显然超出了 Kotlin 协程的范围。

但是,通常这个问题不会影响协程的异步用例。异步协程永远不会被放弃,但最终会一直运行到完成,因此如果协程内部的代码正确关闭其资源,那么它们最终将被关闭。

并发和线程

每个单独的协程,就像一个线程一样,按顺序执行。这意味着以下类型的代码在协程中是完全安全的:

launch { // 启动一个协程
    val m = mutableMapOf<String, String>()
    val v1 = someAsyncTask1() // 启动异步任务
    val v2 = someAsyncTask2() // 启动异步任务
    m["k1"] = v1.await() // map 修改在 await 上等待
    m["k2"] = v2.await() // map 修改在 await 上等待
}

您可以在特定协程的范围内使用所有常规的单线程可变结构。但是,在协程 之间 共享可变状态是有潜在危险的。如果您使用安装调度程序的协程构建器在单个事件调度线程中恢复所有 JS 样式的协程,例如 延续拦截器 章节中显示的 Swing 拦截器,那么您可以安全地与通常从此事件调度线程修改的所有共享对象工作。但是,如果您在多线程环境中工作,或者在不同线程中运行的协程之间共享可变状态,那么您必须使用线程安全(并发)数据结构。

协程在这个意义上就像线程,尽管它们更轻量级。您可以在几个线程上运行数百万个协程。正在运行的协程总是在某个线程中执行。但是,被挂起的 协程不消耗线程,也不会以任何方式绑定到线程。恢复这个协程的挂起函数通过在这个线程上调用 Continuation.resumeWith 来决定协程在哪个线程上恢复,协程的拦截器可以覆盖这个决定并将协程的执行分派到不同的线程上。

异步编程风格

异步编程有不同的风格。

回调在 异步计算 部分中进行了讨论,通常是协程旨在替换的最不方便的样式。任何回调风格的 API 都可以包装到相应的挂起函数中,如 这里 所示。

让我们回顾一下。例如,假设您从假想的 阻塞 函数 sendEmail 开始,其签名如下:

fun sendEmail(emailArgs: EmailArgs): EmailResult

它在运行时可能会长时间阻塞执行线程。

为了使其非阻塞,您可以使用例如错误优先 node.js 回调约定 在回调中表示其非阻塞版本,具有以下签名的样式:

fun sendEmail(emailArgs: EmailArgs, callback: (Throwable?, EmailResult?) -> Unit)

然而,协程支持其他风格的异步非阻塞编程。其中之一是内置于许多流行语言中的 async/await 风格。在 Kotlin 中,可以通过引入作为 futures 用例章节的一部分显示的 future{}.await() 库函数来复制这种风格。

这种风格的约定是从函数返回某种 future 对象,而不是将回调作为参数。在这种异步风格中,sendEmail 的签名将如下所示:

fun sendEmailAsync(emailArgs: EmailArgs): Future<EmailResult>

就风格而言,在此类方法名称中添加 Async 后缀是一种很好的做法,因为它们的参数与阻塞版本没有什么不同,而且很容易忘记它们操作的异步性质。函数 sendEmailAsync 启动一个 并发 异步操作,并可能带来所有并发的陷阱。然而,提倡这种编程风格的语言通常也有某种 await 原语,可以根据需要将执行带回序列中。

Kotlin 的 native 编程风格基于挂起函数。在这种风格中,sendEmail 的签名看起来很自然,没有对其参数或返回类型进行任何修改,但带有一个额外的 suspend 修饰符:

suspend fun sendEmail(emailArgs: EmailArgs): EmailResult

使用我们已经看到的原语,可以轻松地将异步和挂起样式相互转换。例如,sendEmailAsync 可以通过使用 future 协程构建器 挂起 sendEmail 来实现:

fun sendEmailAsync(emailArgs: EmailArgs): Future<EmailResult> = future {
    sendEmail(emailArgs)
}

而挂起函数 sendEmail 可以通过 sendEmailAsync 使用.await()挂起函数来实现

suspend fun sendEmail(emailArgs: EmailArgs): EmailResult = 
    sendEmailAsync(emailArgs).await()

所以,从某种意义上说,这两种风格是等价的,而且在便利性上都绝对优于回调风格。但是,让我们更深入地了解 sendEmailAsync 和挂起 sendEmail 之间的区别。

让我们先比较一下他们是如何 构成 的。挂起函数可以像普通函数一样组成:

suspend fun largerBusinessProcess() {
    // a lot of code here, then somewhere inside
    sendEmail(emailArgs)
    // something else goes on after that
}

相应的异步风格函数以这种方式组成:

fun largerBusinessProcessAsync() = future {
   // a lot of code here, then somewhere inside
   sendEmailAsync(emailArgs).await()
   // something else goes on after that
}

请注意,异步风格的函数组合更加冗长且 容易出错。如果您在异步样式示例中省略 .await() 调用,代码仍然可以编译和工作,但它现在异步执行电子邮件发送过程,甚至与更大的业务流程的其余部分 并发,因此可能会修改一些共享状态和引入一些非常难以重现的错误。相反,挂起函数是 默认的顺序的。使用挂起函数,每当您需要任何并发性时,您都可以使用某种 future{} 或类似的协程构建器调用在源代码中明确表达它。

去比较许多库的大型项目的这些样式如何缩放。挂起函数是 Kotlin 中的轻量级语言概念。所有挂起函数都可以在任何不受限制的 Kotlin 协程中完全使用。异步风格的函数依赖于框架。每个 promise/futures 框架都必须定义自己的类似“async”的函数,该函数返回自己的 promise/future 类和自己的类似“await”的函数。

比较他们的性能。挂起函数每次调用提供最小的开销。您可以查看 实现细节 部分。除了所有挂起机制之外,异步风格的函数还需要保持相当重的 promise/future 抽象。一些类似 future 的对象实例必须始终从异步风格的函数调用中返回,并且即使函数非常简短和简单,也无法对其进行优化。异步风格不适合非常细粒度的分解。

将它们的互操作性与 JVM/JS 代码进行比较。异步风格的函数与使用类似 future 抽象的匹配类型的 JVM/JS 代码具有更好的互操作性。在 Java 或 JS 中,它们只是返回相应的类似 future 的对象的函数。对于任何不支持 延续传递风格 的语言,挂起函数看起来都很奇怪。但是,您可以在上面的示例中看到,对于任何给定的 Promise/Future 框架,将任何挂起函数转换为异步风格的函数是多么容易。因此,您只需在 Kotlin 中编写一次挂起函数,然后使用适当的 future{} 协程构建器函数,通过一行代码为任何风格的 Promise/Future 的互操作性适配它。

包装回调(Wrapping callbacks)

许多异步 API 都有回调样式的接口。标准库中的 suspendCoroutine 挂起函数(参见 挂起函数 部分)提供了一种将任何回调包装到 Kotlin 挂起函数中的简单方法。

有一个简单的模式。假设您有 someLongComputation 函数,带有接收计算结果 Value 的回调。

fun someLongComputation(params: Params, callback: (Value) -> Unit)

您可以使用以下简单的代码将其转换为挂起函数:

suspend fun someLongComputation(params: Params): Value = suspendCoroutine { cont ->
    someLongComputation(params) { cont.resume(it) }
} 

现在这个计算的返回类型是显式的,但它仍然是异步的并且不会阻塞线程。

请注意,kotlinx.coroutines 包含协同取消协程的框架。它提供了类似于 suspendCoroutinesuspendCancellableCoroutine 函数,但支持取消。有关详细信息,请参阅其指南中的 取消部分。

对于更复杂的示例,让我们看一下 异步计算 用例中的 aRead() 函数。它可以通过 Java NIO AsynchronousFileChannel 的挂起扩展函数及其 CompletionHandler回调接口来实现,代码如下:

suspend fun AsynchronousFileChannel.aRead(buf: ByteBuffer): Int =
    suspendCoroutine { cont ->
        read(buf, 0L, Unit, object : CompletionHandler<Int, Unit> {
            override fun completed(bytesRead: Int, attachment: Unit) {
                cont.resume(bytesRead)
            }

            override fun failed(exception: Throwable, attachment: Unit) {
                cont.resumeWithException(exception)
            }
        })
    }

你可以在 这里 获取此代码。注意:kotlinx.coroutines 中的实际实现支持取消以中止长时间运行的 IO 操作。

如果您正在处理许多共享相同类型回调的函数,那么您可以定义一个通用的包装函数来轻松地将它们全部转换为挂起函数。例如,vert.x 使用一个特殊的约定,它的所有异步函数都接收 Handler> 作为回调。为了简化协程中任意 vert.x 函数的使用,可以定义以下辅助函数:

inline suspend fun <T> vx(crossinline callback: (Handler<AsyncResult<T>>) -> Unit) = 
    suspendCoroutine<T> { cont ->
        callback(Handler { result: AsyncResult<T> ->
            if (result.succeeded()) {
                cont.resume(result.result())
            } else {
                cont.resumeWithException(result.cause())
            }
        })
    }

使用这个辅助函数,可以从带有 vx { async.foo(params, it) } 的协程调用任意异步 vert.x 函数 async.foo(params, handler)

构建 future

futures 用例中的 future{} 构建器可以为任何 future 或 promise 原语定义,类似于 协程构建器 部分中解释的 launch{} 构建器:

The future{} builder from futures use-case can be defined for any future or promise primitive similarly to the launch{} builder as explained in 协程构建器 section:

fun <T> future(context: CoroutineContext = CommonPool, block: suspend () -> T): CompletableFuture<T> =
        CompletableFutureCoroutine<T>(context).also { block.startCoroutine(completion = it) }

launch{} 的第一个区别是它返回 CompletableFuture 的实现 ,另一个区别是 future{} 的定义带有一个默认的CommonPool context,以便它的默认执行行为类似于CompletableFuture.supplyAsync 方法,默认在 ForkJoinPool.commonPool 中运行其代。 CompletableFutureCoroutine 的基本实现很简单:

class CompletableFutureCoroutine<T>(override val context: CoroutineContext) : CompletableFuture<T>(), Continuation<T> {
    override fun resumeWith(result: Result<T>) {
        result
            .onSuccess { complete(it) }
            .onFailure { completeExceptionally(it) }
    }
}

你可以在 这里 获取此代码。 kotlinx.coroutines 中的实际实现更高级,因为它传播了对结果future的取消来取消协程。

这个协程的完成会调用 future 对应的 complete 方法来记录这个协程的结果。

非阻塞睡眠(Non-blocking sleep)

协程不应使用 Thread.sleep,因为它会阻塞线程。但是,使用 Java 的 ScheduledThreadPoolExecutor 去实现挂起非阻塞函数 delay 是非常简单直接的。

private val executor = Executors.newSingleThreadScheduledExecutor {
    Thread(it, "scheduler").apply { isDaemon = true }
}

suspend fun delay(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS): Unit = suspendCoroutine { cont ->
    executor.schedule({ cont.resume(Unit) }, time, unit)
}

你可以在 这里 获取此代码。注意:kotlinx.coroutines也提供了delay功能。

请注意,这种 delay 函数会恢复那些在单一 scheduler 线程中使用 delay 函数的协程。使用 拦截器 的协程(如 Swing)不会留在这个线程中执行,因为它们的拦截器会将它们分派到适当的线程中。没有拦截器的协程将留在此调度程序线程中执行。因此,此解决方案便于演示目的,但不是最有效的解决方案。建议在相应的拦截器中实现本地 sleep。

为此目的,对于 Swing 拦截器,非阻塞 sleep 的本机实现应使用专门设计的 Swing Timer:

suspend fun Swing.delay(millis: Int): Unit = suspendCoroutine { cont ->
    Timer(millis) { cont.resume(Unit) }.apply {
        isRepeats = false
        start()
    }
}

你可以在 这里 获取此代码。注意: kotlinx.coroutines 的 delay 实现知道拦截器特定的 sleep 工具,并在适当的情况下自动使用上述方法。

协作式单线程多任务

编写协作式单线程应用程序非常方便,因为您不必处理并发和共享可变状态。 JS、Python 和许多其他语言没有线程,但有协作多任务原语。

协程拦截器 提供了一个简单的工具来确保所有协程都被限制在一个线程中。示例代码 这里 定义了创建单线程执行服务的 newSingleThreadContext() 函数并使其适应协程拦截器的要求。

我们将它与在以下示例中的 构建 future 部分中定义的 future{} 协程构建器一起使用,该构建器在单个线程中工作,尽管它内部有两个异步任务都活跃。

fun main(args: Array<String>) {
    log("Starting MyEventThread")
    val context = newSingleThreadContext("MyEventThread")
    val f = future(context) {
        log("Hello, world!")
        val f1 = future(context) {
            log("f1 is sleeping")
            delay(1000) // sleep 1s
            log("f1 returns 1")
            1
        }
        val f2 = future(context) {
            log("f2 is sleeping")
            delay(1000) // sleep 1s
            log("f2 returns 2")
            2
        }
        log("I'll wait for both f1 and f2. It should take just a second!")
        val sum = f1.await() + f2.await()
        log("And the sum is $sum")
    }
    f.get()
    log("Terminated")
}

你可以在 这里 获得完整的工作示例。注意:kotlinx.coroutines 有 newSingleThreadContext 的现成实现。

如果您的整个应用程序基于单线程执行,您可以为您的单线程执行工具定义自己的辅助协程构建器,并使用硬编码的context。

异步序列(Asynchronous sequences)

受限挂起 部分中显示的 sequence{} 协程构建器是 同步 协程的示例。只要它的消费者调用 Iterator.next(),它在协程中的生产者代码就会在同一个线程中同步调用。 sequence{} 协程块受到限制,它不能使用第三方挂起函数(如异步文件 IO)挂起其执行,如 包装回调 部分所示。

异步 序列构建器可以任意挂起和恢复其执行。这意味着当数据还没有产生时,它的消费者应该准备好处理这个情况。这是挂起函数的自然用例。让我们定义类似于常规 Iterator 接口的 SuspendingIterator 接口,但它的 next( )hasNext() 函数是可挂起的:

interface SuspendingIterator<out T> {
    suspend operator fun hasNext(): Boolean
    suspend operator fun next(): T
}

SuspendingSequence 的定义类似于标准的 Sequence 但它返回 SuspendingIterator

interface SuspendingSequence<out T> {
    operator fun iterator(): SuspendingIterator<T>
}

我们还为它定义了一个类似于同步序列的范围的范围接口(scope interface),但它挂起不受限制:

interface SuspendingSequenceScope<in T> {
    suspend fun yield(value: T)
}

构建器函数 suspendingSequence{} 类似于同步 sequence{}。它们的区别在于 SuspendingIteratorCoroutine 的实现细节以及在这种情况下接受可选上下文是有意义的:

fun <T> suspendingSequence(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend SuspendingSequenceScope<T>.() -> Unit
): SuspendingSequence<T> = object : SuspendingSequence<T> {
    override fun iterator(): SuspendingIterator<T> = suspendingIterator(context, block)
}

你可以得到完整的代码 这里。注意:kotlinx.coroutines 有一个 Channel 原语的实现以及相应的 produce{} 协程构建器,它提供了更灵活的相同概念的实现。

让我们从 协作式单线程多任务 章节获取 newSingleThreadContext{} 上下文,从 [non-blocking sleep](#non-blocking-sleep)章节获取非阻塞 delay 函数。这样我们可以编写一个非阻塞序列的实现,它产生从 1 到 10 的整数,在它们之间休眠 500 毫秒:

val seq = suspendingSequence(context) {
    for (i in 1..10) {
        yield(i)
        delay(500L)
    }
}

现在消费者协程可以按照自己的节奏消费这个序列,同时也可以使用其他任意挂起函数来挂起。请注意,Kotlin for 循环 按约定工作,因此不需要特殊的 await for 循环构造语言。常规的 for 循环可用于迭代我们上面定义的异步序列。只要生产者没有值,它就会被挂起:

for (value in seq) { // 当等待生产者时,挂起
    // 用 value 做一些操作,当然也可以挂起
}

您可以在 此处 找到一个带有一些日志记录的已解决示例

通道(Channels)

Go 风格的类型安全通道可以在 Kotlin 中作为库实现。我们可以定义一个带有挂起函数send的发送通道接口:

interface SendChannel<T> {
    suspend fun send(value: T)
    fun close()
}

还有接收器通道,具有挂起函数 receiveoperator iterator,其风格类似于 异步序列:

interface ReceiveChannel<T> {
    suspend fun receive(): T
    suspend operator fun iterator(): ReceiveIterator<T>
}

Channel 类实现了这两个接口。当通道缓冲区满时,send 挂起,而当缓冲区为空时,receive 挂起。它允许我们将 Go 风格的代码几乎逐字复制到 Kotlin 中。从 Go 之旅的第四个并发示例 将 n 斐波那契数发送到通道的 fibonacci 函数在 Kotlin 中如下所示:

suspend fun fibonacci(n: Int, c: SendChannel<Int>) {
    var x = 0
    var y = 1
    for (i in 0..n - 1) {
        c.send(x)
        val next = x + y
        x = y
        y = next
    }
    c.close()
}

我们还可以定义 Go 风格的 go {...} 块来在某种多线程池中启动新的协程,该池将任意数量的轻量级协程分派到固定数量的实际重量级线程上。示例实现( 这里 )是在 Java 常见的 ForkJoinPool上。

使用这个 go 协程构建器,相应 Go 代码中的 main 函数将如下所示,其中 mainBlockingrunBlocking 的快捷帮助函数,与 go{} 使用相同的池:

fun main(args: Array<String>) = mainBlocking {
    val c = Channel<Int>(2)
    go { fibonacci(10, c) }
    for (i in c) {
        println(i)
    }
}

你可以在 这里 检出工作代码

您可以随意使用通道的缓冲区大小。为简单起见,示例中仅实现了缓冲通道(最小缓冲区大小为 1),因为无缓冲通道在概念上类似于之前介绍的 异步序列。

Go 风格的 select 控制块在其中一个通道上的某个操作变得可用之前挂起,可以作为 Kotlin DSL 实现,因此 Go 之旅的第 5 个并发示例 在 Kotlin 中看起来像这样:

suspend fun fibonacci(c: SendChannel<Int>, quit: ReceiveChannel<Int>) {
    var x = 0
    var y = 1
    whileSelect {
        c.onSend(x) {
            val next = x + y
            x = y
            y = next
            true // continue while loop
        }
        quit.onReceive {
            println("quit")
            false // break while loop
        }
    }
}

你可以在 这里 检出工作代码

Example 有一个 select {...} 的实现,它返回其中一种情况的结果,例如 Kotlin when 表达式,以及一个方便的 whileSelect { ... } 的实现,它与 while(select { ... }) 相同,但括号更少。

Go 之旅的第 6 个并发示例 中的默认选择案例只是在 select {...} DSL 中添加了一个案例:

fun main(args: Array<String>) = mainBlocking {
    val tick = Time.tick(100)
    val boom = Time.after(500)
    whileSelect {
        tick.onReceive {
            println("tick.")
            true // continue loop
        }
        boom.onReceive {
            println("BOOM!")
            false // break loop
        }
        onDefault {
            println("    .")
            delay(50)
            true // continue loop
        }
    }
}

你可以在 这里 检出工作代码

Time.tickTime.after 在 这里 用非阻塞 delay 函数实现。

可以在 这里 找到其他示例以及注释中相应 Go 代码的链接。

请注意,此通道示例实现基于单个锁来管理其内部等待列表。它使理解和推理变得更容易。但是,它永远不会在此锁下运行用户代码,因此它是完全并发的。这个锁只是在一定程度上限制了它对大量并发线程的可伸缩性。

kotlinx.coroutines 中通道和 select 的实际实现是基于无锁的不相交访问并行(disjoint-access-parallel)的数据结构。

此通道实现独立于协程上下文中的拦截器。它可以在事件线程拦截器下的 UI 应用程序中使用,如相应的 延续拦截器 部分所示,或者与任何其他拦截器一起使用,或者根本没有拦截器(在后一种情况下,执行线程仅由协程中使用的其他挂起函数的代码确定)。通道实现只提供线程安全的非阻塞挂起功能。

互斥锁(Mutexes)

编写可扩展的异步应用程序是一个遵循的原则,确保代码永远不会阻塞,但是挂起(使用挂起函数)可以,不会真正阻塞线程。 Java 并发原语如 ReentrantLock 是线程阻塞的,它们不应该是在真正的非阻塞代码中使用。为了控制对共享资源的访问,可以定义 Mutex 类来挂起协程的执行而不是阻塞它。相应类的 header 如下所示:

class Mutex {
    suspend fun lock()
    fun unlock()
}

你可以在 这里 获得完整的实现。 kotlinx.coroutines 中的实际实现有一些额外的函数。

使用这种非阻塞互斥锁的实现,Go Tour的第9个并发示例可以翻译成Kotlin,通过 Kotlin 的 try-finally,与 Go 的defer 的目的相同:

class SafeCounter {
    private val v = mutableMapOf<String, Int>()
    private val mux = Mutex()

    suspend fun inc(key: String) {
        mux.lock()
        try { v[key] = v.getOrDefault(key, 0) + 1 }
        finally { mux.unlock() }
    }

    suspend fun get(key: String): Int? {
        mux.lock()
        return try { v[key] }
        finally { mux.unlock() }
    }
}

你可以在 这里 检出工作代码

从实验协程迁移

协程是 Kotlin 1.1-1.2 中的一个实验性功能。相应的 API 在 kotlin.coroutines.experimental 包中公开。自 Kotlin 1.3 起可用的稳定版协程使用 kotlin.coroutines 包。实验包仍然在标准库中可用,并且使用实验协程编译的代码仍然可以像以前一样工作。

Kotlin 1.3 编译器支持调用实验性挂起函数并将挂起 lambdas 传递给使用实验性协程编译的库。在幕后,创建了相应的稳定和实验协程接口之间的适配器。

参考

  • 进一步阅读:
    • 请先阅读! 协程参考指南 。
  • 演示文稿:
    • 协程简介 (Roman Elizarov at KotlinConf 2017, 幻灯片)
    • 深入了解协程 (Roman Elizarov at KotlinConf 2017, 幻灯片)
    • Kotlin 协程实践(Roman Elizarov 在 KotlinConf 2018,幻灯片)
  • 语言设计概述:
    • 第 1 部分(原型设计):Kotlin 中的协程(Andrey Breslav 在 JVMLS 2016)
    • 第 2 部分(当前设计):Kotlin 协程重装上阵(Roman Elizarov 在 JVMLS 2017,幻灯片)

你可能感兴趣的:(Kotlin,kotlin)