- 类型:设计提案
- 作者:Andrey Breslav, Roman Elizarov
- 贡献者: Vladimir Reshetnikov, Stanislav Erokhin, Ilya Ryzhenkov, Denis Zharkov
- 状态:从 Kotlin 1.3 开始稳定,在 Kotlin 1.1-1.2 中为实验性
- 英文原文地址: https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md
本文是对 Kotlin 协程的描述。这一概念通常被认为与下列内容有关,或部分涵盖它们:
- generators/yield
- async/await
- composable/delimited сontinuations
设计目标:
- 不依赖 Future 之类复杂的库提供的特定实现;
- 同时涵盖 “async/await” 用例以及“生成器代码块”;
- 使 Kotlin 协程能包装各种现有的异步 API (如 Java NIO、各种 Future 的实现等);
协程可以被视作可挂起的计算的实例。即,可以在某些点上挂起,稍后在另一个线程上恢复执行。协程相互调用(互相传递数据),即可形成协作式多任务处理机制。
最能描述协程功能的用例是异步计算(在 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
参数显式传递给回调,它们只是将它视为闭包的一部分),但缩进级别每次都在增长,而且只要嵌套超过一层,大家都知道会产生多少麻烦(谷歌搜索“回调地狱”,看看 JavaScript 迫害了多少人)。
同样的计算可以直截了当地表达为协程(前提是有一个合适的库,使 IO 的 API 适配协程的需求):
launch {
// 异步读时挂起
val bytesRead = inChannel.aRead(buf)
// 读完成后才执行这一行
...
...
process(buf, bytesRead)
// 异步写时挂起
outChannel.aWrite(buf)
// 写完成后才执行这一行
...
...
outFile.close()
}
这里的 aRead()
与 aWrite()
是特殊的挂起函数——它们可以挂起代码执行(这并不意味着阻塞正在运行它的线程),并在调用完成时恢复。如果我们眯起眼睛,可以想象所有在 aRead()
之后的代码已经被包装成一个
lambda 表达式并作为回调传递给 aRead()
,对 aWrite()
也是如此,我们就可以看到这段代码与上面的相同,可读性却更强。
我们的明确目标是以一种非常通用的方式支持协程,所以在此示例中,launch{}
、.aRead()
以及 .aWrite()
只是适应协程工作的库函数;launch
是协程构建器——它创建并启动协程,而 aRead()
与 aWrite()
作为特殊的挂起函数,它隐式地接受续体(续体就是普通的回调)。
注意,显式传入的回调要在循环中异步调用通常非常棘手,但在协程中这不过是稀松平常的小事:
launch {
while (true) {
// 异步读时挂起
val bytesRead = inFile.aRead(buf)
// 读完继续执行
if (bytesRead == -1) break
...
process(buf, bytesRead)
// 异步写时挂起
outFile.aWrite(buf)
// 写完继续执行
...
}
}
可想而知,在协程中处理异常也会稍微方便一些。
还有另一种表达异步计算的方式:通过 future(也称为 promise 或 deferred)。我们将在示例中使用一个虚构的 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())
}
同样,协程通过更少的缩进以及更自然的组合逻辑(以及异常处理,这里没有显示),而且没有使用专门的关键字(比如 C#、JS 以及其他语言中的 async
与 await
)来支持 future:future{}
以及 .await()
都只是库函数而已。
协程的另一个典型用例是延时计算序列(在 C#、Python 以及许多其他语言中通过 yield
实现)。这样的序列可以由看似顺序的代码生成,但在运行时只计算所请求的元素:
// 推断出类型为 Sequence<Int>
val fibonacci = sequence {
yield(1) // 斐波那契数列的首项
var cur = 1
var next = 1
while (true) {
yield(next) // 斐波那契数列的下一项
val tmp = cur + next
cur = next
next = tmp
}
}
代码创建了一个表示斐波那契数列的延迟序列,它可以是无限长的(类似 Haskell 的无限长列表)。我们可以只计算其中一部分,例如,通过 take()
:
println(fibonacci.take(10).joinToString())
这会打印出
1, 1, 2, 3, 5, 8, 13, 21, 34, 55
。你可以在这里试一下。
生成器的优势在于支持任意的控制流,包括但不限于 while
、if
、try
/catch
/finally
:
val seq = sequence {
yield(firstItem) // 挂起点
for (item in input) {
if (!item.isValid()) break // 不再生成项
val foo = item.toFoo()
if (!foo.isGood()) continue
yield(foo) // 挂起点
}
try {
yield(lastItem()) // 挂起点
}
finally {
// 一些收尾代码
}
}
关于
sequence{}
与yield()
的示例代码在限定挂起一节。
注意,这种方法还允许把 yieldAll(sequence)
表示为库函数(像 sequence{}
与 yield()
那样),这能简化延时序列的连接操作,并且提升了性能。
典型的 UI 应用程序只有一个事件调度线程,所有 UI 操作都发生在这个线程上。通常不允许在其他线程修改 UI 状态。所有 UI 库都提供某种原语,以将操作转移回 UI 线程中执行。例如,Swing 的
SwingUtilities.invokeLater
,JavaFX 的
Platform.runLater
,Android 的
Activity.runOnUiThread
等等。下面是一个典型的 Swing 应用程序的代码片段,它执行一些异步操作,然后在 UI 中显示其结果:
makeAsyncRequest {
// 异步请求完成时执行这个 lambda 表达式
result, exception ->
if (exception == null) {
// 在 UI 线程显示结果
SwingUtilities.invokeLater {
display(result)
}
} else {
// 异常处理
}
}
这很像我们之前在异步计算用例中见过的回调地狱,所以也能通过协程优雅地解决:
launch(Swing) {
try {
// 执行异步请求时挂起
val result = makeRequest()
// 在 UI 上显示结果,Swing 上下文保证了我们位于事件调度线程上
display(result)
} catch (exception: Throwable) {
// 异常处理
}
}
Swing
上下文的示例代码在续体拦截器一节。
所有的异常处理也都可以使用原生的语法结构执行。
协程可以覆盖更多用例,比如下面这些:
- 基于通道的并发(就是 Go 协程与通道);
- 基于 Actor 模式的并发;
- 偶尔需要用户交互的后台进程,例如显示模式对话框;
- 通信协议:将每个参与者实现为一个序列,而不是状态机;
- Web 应用程序工作流:注册用户、验证电子邮件、登录(挂起的协程可以序列化并存储在数据库中)。
本部分概述了支持编写协程的语言机制以及管理其语义的标准库。
-
协程——可挂起计算的实例。它在概念上类似于线程,在这个意义上,它需要一个代码块运行,并具有类似的生命周期 —— 它可以被创建与启动,但它不绑定到任何特定的线程。它可以在一个线程中挂起其执行,并在另一个线程中恢复。而且,像 future 或 promise 那样,它在完结时可能伴随着某种结果(值或异常)。
-
挂起函数——
suspend
修饰符标记的函数。它可能会通过调用其他挂起函数挂起执行代码,而不阻塞当前执行线程。挂起函数不能在常规代码中被调用,只能在其他挂起函数或挂起 lambda 表达式中(见下方)。例如,用例所示的.await()
与yield()
是在库中定义的挂起函数。标准库提供了原始的挂起函数,用于定义其他所有挂起函数。 -
挂起 lambda 表达式——必须在协程中运行的代码块。它看起来很像一个普通的 lambda 表达式,但它的函数类型被
suspend
修饰符标记。就像常规 lambda 表达式是匿名局部函数的短语法形式一样,挂起 lambda 表达式是匿名挂起函数的短语法形式。它可能会通过调用其他挂起函数挂起执行代码,而不阻塞当前执行线程。例如,用例所示的跟在launch
、future
以及BuildSequence
函数后面花括号里的代码块就是挂起 lambda 表达式。注意:常规 lambda 表达式可以在其代码的任意位置调用挂起函数,只要这个位置能编写从这个 lambda 表达式非局部
return
的语句。也就是说,可以在像apply{}
代码块这样的内联 lambda 表达式中调用挂起函数,但在noinline
与crossinline
修饰的 lambda 表达式中就不行。挂起会被视作是一种特殊的非局部控制转移。 -
挂起函数类型——表示挂起函数以及挂起 lambda 表达式的函数类型。它就像一个普通的函数类型,但具有
suspend
修饰符。举个例子,suspend () -> Int
是一个没有参数、返回Int
的挂起函数的函数类型。一个声明为suspend fun foo() : Int
的挂起函数符合上述函数类型。 -
协程构建器——使用一些挂起 lambda 表达式作为参数来创建一个协程的函数,并且可选地,还提供某种形式以访问协程的结果。例如,用例中的
launch{}
、future{}
以及sequence{}
就是协程构建器。标准库提供了用于定义其他所有协程构建器所使用的原始协程构建器。注意:一些语言通过对特定方法的硬编码支持协程的创建、启动、定义其执行的方式以及结果的表示方式。例如,
generate
关键字可以定义返回某种可迭代对象的协程,而async
关键字可以定义返回某种约定或任务的协程。Kotlin 没有关键字或修饰符来定义以及启动协程。协程构建器只是库中定义的简单的函数。其他语言中以方法体形式定义的协程,在 Kotlin 中,这样的方法通常是具有表达式方法体的普通方法,方法体的内容是调用一个库中定义的、最后一个参数是挂起 lambda 表达式的协程构建器:fun doSomethingAsync() = async { ... }
-
挂起点——协程执行过程中可能被挂起的位置。从语法上说,挂起点是对一个挂起函数的调用,但实际的挂起在挂起函数调用了标准库中的原始挂起函数时发生。
-
续体——是挂起的协程在挂起点时的状态。它在概念上表示在挂起点之后的剩余应执行的代码。例如:
sequence { for (i in 1..10) yield(i * i) println("over") }
这里,每次调用挂起函数
yield()
时,协程都会挂起,其执行的剩余部分被视作续体,所以有 10 个续体:循环运行第一次后,i=2
,挂起;循环运行第二次后,i=3
,挂起……最后一次打印“over”并完结协程。已经创建,但尚未启动的协程,由它的初始续体表示,这由它的整个执行组成,类型为Continuation<Unit>
。
如上所述,驱动协程的要求之一是灵活性:我们希望能够支持许多现有的异步 API 以及其他用例,并尽量减少硬编码到编译器中的部分。因此,编译器只负责支持挂起函数、挂起 lambda 表达式以及相应的挂起函数类型。标准库中的原语很少,其余的则留给应用程序库。
这是标准库中接口 Continuation
的定义(位于 kotlinx.coroutines
包),代表了一个通用的回调:
interface Continuation<in T> {
val context: CoroutineContext
fun resumeWith(result: Result<T>)
}
上下文将在协程上下文一节中详细介绍,表示与协程关联的任意用户定义上下文。resumeWIth
函数是一个完结回调,用于报告协程完结时成功(带有值)或失败(带有异常)的结果。
为了方便,包里还定义了两个扩展函数:
fun <T> Continuation<T>.resume(value: T)
fun <T> Continuation<T>.resumeWithException(exception: Throwable)
一个典型的挂起函数的某种实现,例如 .await()
,看起来是这样的:
suspend fun <T> CompletableFuture<T>.await(): T =
suspendCoroutine<T> { cont: Continuation<T> ->
whenComplete { result, exception ->
if (exception == null) // 这个 future 正常完结了
cont.resume(result)
else // 这个 future 因为异常而完结了
cont.resumeWithException(exception)
}
}
你可以在这里找到代码。注意:这个简单的实现只要 future 不完结就会永远挂起协程。kotlinx.coroutines 中的实际实现还支持取消。
suspend
修饰符表明这个函数可以挂起协程的执行。这个特殊的函数被定义为类型 CompletableFuture<T>
的扩展函数,以便使用它时能自然按照与实际执行顺序相对应的从左到右的顺序读取:
doSomethingAsync(...).await()
suspend
修饰符可以用于任何函数:顶层函数、扩展函数、成员函数、局部函数或操作符函数。
属性的取值器与设值器、构造函数以及某些操作符函数(也就是
getValue
,setValue
,provideDelegate
,get
,set
以及equals
)不能带有suspend
修饰符。这些限制将来可能会被移除。
挂起函数可以调用任何常规函数,但要真正挂起执行,必须调用一些其他的挂起函数。特别是,这个 await
实现调用了在标准库中定义的顶层挂起函数 suspendCoroutine
(位于 kotlinx.coroutines
包):
suspend fun <T> suspendCoroutine(block: (Continuation<T>) -> Unit): T
当 suspendCoroutine
在一个协程中被调用时(它只可能在协程中被调用,因为它是一个挂起函数),它捕获了协程的执行状态到一个续体实例,然后将其作为参数传递给指定的 block
。为了恢复协程的执行,代码块需要在该线程或稍后在其他某个线程中调用 continuation.resumeWith()
(直接调用 continuation.resume()
或 continuation.resumeWithException()
扩展)。当 suspendCoroutine
代码块没有调用 resumeWith
就返回时,会发生实际的协程挂起。如果续体还未从代码块返回就直接被恢复,协程就不被看作已经暂停又继续执行。
传给 continuation.resumeWith()
的值作为调用 suspendCoroutine
的结果,进一步成为 .await()
的结果。
不允许多次恢复同一个协程,并会产生 IllegalStateException
。
注意:这正是 Kotlin 协程与像 Scheme 这样的函数式语言中的顶层限定续体或 Haskell 中的续体单子的关键区别。我们选择仅支持续体恢复一次,完全是出于实用主义考虑,因为所有这些预期的用例都不需要多重续体。然而,还是可以在单独的库中实现多重续体,通过底层的所谓协程内建函数复制续体中捕获的协程状态,然后就可以从这个副本再次恢复协程。
挂起函数不能从常规函数中调用,所以标准库提供了用于在常规非挂起作用域中启动协程执行的函数。这是简化的协程构建器 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
接口的快捷方式,该接口具有其 context
的给定值以及
resumeWith
函数的主体。这个续体作为完结续体被传递给
block.startCoroutine(...)
扩展函数(来自 kotlin.coroutines
包)。
协程在完结时将调用其完结续体。其 resumeWith
函数将在协程因成功或失败而完结时调用。因为 launch
是那种“即发即弃”式的协程,它被定义成返回 Unit
的挂起函数,实际上是忽略了
resume
函数的结果。如果协程因异常完结,当前线程的未捕获异常句柄将用于报告这个异常。
注意:这个简单实现返回了
Unit
并且根本不提供对协程状态的任何访问。实际上在 kotlinx.coroutines 中的实现要更加复杂,因为它返回了一个代表这个协程的Job
接口的实例,而且可以被取消。
上下文在协程上下文一节中详细介绍。startCoroutine
在标准库中定义为无参数或单参数的挂起函数类型的扩展函数:
fun <T> (suspend () -> T).startCoroutine(completion: Continuation<T>)
fun <R, T> (suspend R.() -> T).startCoroutine(receiver: R, completion: Continuation<T>)
startCoroutine
创建协程并在当前线程中立刻启动执行(但请参阅下面的备注),直到第一个挂起点时返回。挂起点是协程中某个挂起函数的调用,由相应的挂起函数的代码来定义协程恢复的时机与方式。
注意:续体拦截器(来自上下文)在后文中会提到,它能够将协程的执行,包括其初始续体的执行,调度到另一个线程中。
协程上下文是一组可以附加到协程中的持久化用户定义对象。它可以包括负责协程线程策略的对象,日志,关于协程执行的安全性以及事务方面的对象,协程的标识与名称等等。下面是协程及其上下文的简单认识模型。把协程看作一个轻量线程。在这种情况下,协程上下文就像是一组线程局部变量。不同之处在于线程局部变量是可变的,而协程上下文是不可变的,但对于协程,这并不是一个严重的限制,因为他们是如此轻量以至于当需要改变上下文时可以很容易地开启一个新的协程。
标准库没有包含上下文的任何具体实现,但是有接口与抽象类,以便以可组合的方式在库中定义所有这些方面,因此来自不同库的各个方面可以在同一个上下文中和平共存。
从概念上讲,协程上下文是一组索引元素,其中每个元素有唯一的键。它是 set 与 map 的混合体。它的元素有像在 map 中的那样的键,但它的键直接与元素关联,更像是 set。标准库定义了
CoroutineContext
的最小接口(位于 kotlinx.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
支持通过给定键类型安全地访问元素。可以使用[..]
写法,解释见 Kotlin 操作符重载。 - 函数
fold
类似于标准库中Collection.fold
扩展函数,提供迭代上下文中所有元素的方法。 - 操作符
plus
类似于标准库的Set.plus
扩展函数,返回两个上下文的组合, 同时加号右边的元素会替换掉加号左边具有相同键的元素。 - 函数
minusKey
返回不包含指定键的上下文。
协程上下文的一个 Element
就是上下文本身。那是仅有这一个元素的上下文单例。这样就可以通过获取库定义的协程上下文元素并使用 +
连接它们,来创建一个复合上下文。例如,如果一个库定义的 auth
元素带着用户授权信息,而另一些库定义了带有一些带有上下文执行信息的 threadPool
对象,你就可以使用协程构建器 launch{}
使用组合上下文使用
launch(auth + CommonPool){...}
调用。
注意:kotlinx.coroutines 提供了几个上下文元素,包括用于在一个共享后台线程池中调度协程的
Dispatchers.Default
对象。
标准库提供
EmptyCoroutineContext
——一个不包含任何元素的(空的)CoroutineContext
实例。
所有第三方协程元素应该继承标准库的
AbstractCoroutineContextElement
类(位于 kotlinx.coroutines
包)。要在库中定义上下文元素,建议使用以下风格。以下示例展示了存储当前用户名的假设授权上下文元素:
class AuthUser(val name: String) : AbstractCoroutineContextElement(AuthUser) {
companion object Key : CoroutineContext.Key<AuthUser>
}
可以在这里找到示例。
将上下文的 Key
定义为相应元素类的伴生对象能够流畅访问上下文中的相应元素。这是一个假想的的挂起函数实现,它需要检查当前用户名:
suspend fun doSomething() {
val currentUser = coroutineContext[AuthUser]?.name ?: throw SecurityException("unauthorized")
// 做一些用户指定的事
}
它使用了 coroutineContext
顶层属性(位于 kotlinx.coroutines
包),以用于在挂起函数中检索当前协程的上下文。
让我们回想一下异步 UI 用例。异步 UI 应用程序必须保证协程程序体始终在 UI 线程中执行,尽管事实上各种挂起函数是在任意的线程中恢复协程执行。这是使用续体拦截器完成的。首先,我们要充分了解协程的生命周期。思考一下这个使用了协程构建器 launch{}
的代码片段:
launch(CommonPool) {
initialCode() // 执行初始化代码
f1.await() // 挂起点 #1
block1() // 执行 #1
f2.await() // 挂起点 #2
block2() // 执行 #2
}
协程从 initialCode
开始执行,直到第一个挂起点。在挂起点时,协程挂起,一段时间后按照相应挂起函数的定义,协程恢复并执行
block1
,接着再次挂起又恢复后执行 block2
,在此之后协程完结了。
续体拦截器可以选择拦截并包装与 initialCode
、block1
以及 block2
执行相对应的、从它们恢复的位置到下一个挂起点之间的续体。协程的初始化代码被视作是由协程的初始续体恢复得来。标准库提供了
ContinuationInterceptor
接口(位于 kotlinx.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
:
val intercepted = continuation.context[ContinuationInterceptor]?.interceptContinuation(continuation) ?: continuation
协程框架为每个实际的续体实例缓存拦截过的续体,并且当不再需要它时调用 releaseInterceptedContinuation(intercepted)
。想了解更多细节请参阅实现细节部分。
注意,像
await
这样的挂起函数实际上不一定会挂起协程的执行。例如,挂起函数小节所展现的await
实现在 future 已经完结的情况下就不会使协程真正挂起(在这种情况下resume
会立刻被调用,协程的执行并没有被挂起)。只有协程在执行中真正被挂起时,续体才会被拦截,即suspendCoroutine
块返回而不调用resume
。
让我们来看看 Swing
拦截器的具体示例代码,它将执行调度到
Swing UI 事件调度线程上。我们先来定义一个包装类 SwingContinuation
,它调用 SwingUtilities.invokeLater
,把续体调度到 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
对象并实现 ContinuationInterceptor
接口,用作对应的上下文元素:
object Swing : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
SwingContinuation(continuation)
}
你可以从这里获得这部分代码。注意:
Swing
对象在 kotlinx.coroutines 中的实际实现还支持了协程调试功能,提供对当前协程的标识符的访问以及显示,标识符用运行协程的线程表示。
现在,可以用带有 Swing
参数的协程构建器 launch{}
来执行完全运行在 Swing 事件调度线程中的协程:
launch(Swing) {
// 这里的代码可以挂起,但总是恢复在 Swing 事件调度线程上
}
在 kotlinx.coroutines 中,Swing 上下文的实际实现更加复杂,因为它还要集成库的计时与调试工具。
为了实现生成器用例中的 sequence{}
与 yield()
,需要另一类协程构建器与挂起函数。以下是协程构建器 sequence{}
的示例代码:
fun <T> sequence(block: suspend SequenceScope<T>.() -> Unit): Sequence<T> = Sequence {
SequenceCoroutine<T>().apply {
nextStep = block.createCoroutine(receiver = this, completion = this)
}
}
它使用了标准库中类似于 startCoroutine
(解释见协程构建器小节)的另一个原语
createCoroutine
。不同点在于它创建一个协程,但并不启动协程,而是返回表示协程的初始续体作为 Continuation<Unit>
的引用:
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 表达式 block
是具有 SequenceScope<T>
接收者的扩展 lambda 表达式。SequenceScope<T>
接口提供了生成器代码块的作用域,其在库中定义如下:
interface SequenceScope<in T> {
suspend fun yield(value: T)
}
为了避免生成多个对象,sequence{}
实现中定义了 SequenceCoroutine<T>
类,它同时实现了 SequenceScope<T>
与 Continuation<Unit>
,因此它可以同时作为 createCoroutine
的 receiver
参数与 completion
续体参数。下面展示了 SequenceCoroutine<T>
的一种简单实现:
private class SequenceCoroutine<T>: AbstractIterator<T>(), SequenceScope<T>, Continuation<Unit> {
lateinit var nextStep: Continuation<Unit>
// 实现抽象迭代器
override fun computeNext() { nextStep.resume(Unit) }
// 实现续体
override val context: CoroutineContext get() = EmptyCoroutineContext
override fun resumeWith(result: Result<Unit>) {
result.getOrThrow() // 错误则退出
done()
}
// 实现生成器
override suspend fun yield(value: T) {
setNext(value)
return suspendCoroutine { cont -> nextStep = cont }
}
}
你可以从这里获得该代码。注意,标准库提供了
sequence
函数开箱即用的优化实现(位于kotlinx.coroutines
包),而且还具有对yieldAll
函数的额外支持。
sequence
的实际代码使用了实验性的BuilderInference
特性以支持生成器一节中使用的,不用显式指定序列类型参数T
的fibonacci
声明。相反,其类型是从传递给yield
的参数类型推断得来的。
yield
的实现中使用了 suspendCoroutine
挂起函数来挂起协程并捕获其续体。续体保存在 nextStep
中,并在调用 computeNext
时恢复。
然而,之前展示的 sequence{}
与 yield()
,其续体并不能被任意的挂起函数在各自的作用域里捕获。它们同步地工作。它们需要对如何捕获续体、在何处存储续体以及何时恢复续体保持绝对的控制。它们形成了限定挂起域。对挂起的限定作用由作用域类或接口上的 RestrictSuspension
注解提供,在上面的示例中这个作用域接口是 SequenceScope
:
@RestrictsSuspension
interface SequenceScope<in T> {
suspend fun yield(value: T)
}
这个注解对能用在 sequence{}
域或其他类似的同步协程构建器中的挂起函数有一定的限制。那些扩展限定性挂起域类或接口(以 @RestrictsSuspension
标记)的挂起 lambda 表达式或函数称作限定性挂起函数。限定性挂起函数只接受来自同一个限定挂起域实例的的成员或扩展挂起函数作为参数。
回到这个例子,这意味着 SequenceScope
作用域的内扩展 lambda 表达式不能调用 suspendContinuation
或其他通用挂起函数。要挂起 sequence
协程的执行,最终必须通过调用 SequenceScope.yield
。yield
本身被实现为 SequenceScope
实现的成员函数,对其内部不作任何限制(只有扩展挂起 lambda 表达式与函数是限定的)。
对于像 sequence
这样的限定性协程构建器,支持任意上下文是没有意义的,因为其作用类或接口(比如这个例子里的 SequenceScope
)已经占用了上下文能提供的服务,因此限定性协程只能使用 EmptyCoroutineContext
作为上下文,SequenceCouroutine
的取值器实现也会返回这个。尝试创建上下文不是 EmptyCoroutineSContext
的限定性协程会引发 IllegalArgumentException
。
本节展现了协程实现细节的冰山一角。它们隐藏在协程概述部分解释的构建代码块背后,内部类与代码的生成策略随时可能变化,只要不打破公共 API 与 ABI 的约定。
挂起函数通过 Continuation-Passing-Style(CPS, 续体传递风格)实现。每个挂起函数与挂起 lambda 表达式都有一个附加的 Continuation
参数,在调用时隐式传入。回想一下,await
挂起函数的声明是这样的:
suspend fun <T> CompletableFuture<T>.await(): T
然而在 CPS 变换之后,它的实际实现具有以下签名:
fun <T> CompletableFuture<T>.await(continuation: Continuation<T>): Any?
其返回类型 T
移动到了附加的续体参数的类型参数位置。实现中的返回值类型 Any?
被设计用于表示挂起函数的动作。当挂起函数挂起协程时,函数返回一个特别的标识值
COROUTINE_SUSPENDED
(更多细节参考协程内建函数
一节)。如果一个挂起函数没有挂起协程,协程继续执行时,它直接返回一个结果或者抛出一个异常。这样,await
函数实现中的返回值类型 Any?
实际上是 T
与 COROUTINE_SUSPENDED
的联合类型,这并不能在 Kotlin 的类型系统中表示出来。
挂起函数的实现实际上不允许直接调用其栈帧中的续体,因为在长时间运行的协程中这可能导致栈溢出。标准库中的 suspendCoroutine
函数通过追踪续体的调用对应用开发者隐藏这种复杂性,并确保无论续体在何时怎样调用,都与挂起函数的实际实现具有一致性。
协程实现的性能至关重要,这需要尽可能少地创建类与对象。许多语言通过状态机实现,Kotlin 也是这样做的。对于 Kotlin,使用此方法使得无论挂起 lambda 表达式体内有多少挂起点,编译器也只创建一个类。
主要思想:挂起函数编译为状态机,其状态对应着挂起点。示例:编写一个有两个挂起点的挂起代码块:
val a = a()
val y = foo(a).await() // 挂起点 #1
b()
val z = bar(a, y).await() // 挂起点 #2
c(z)
这个代码块有三个状态:
- 初始化(在所有挂起点之前)
- 在第一个挂起点之后
- 在第二个挂起点之后
每个状态都是这个代码块某个续体的入口点(初始续体从第一行开始)。
代码会被编译为一个匿名类,它的一个方法实现了这个状态机、一个字段持有状态机当前状态,状态之间共享协程的局部变量字段(也可能有协程闭包的字段,但在这种情况下它是空的译者注:未能 get 到 它
指代什么)。这是上文代码块通过 CPS 调用挂起函数 await
的 Java 伪代码:
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
}
}
请注意,这里有 goto
操作符,还有标签,因为该示例描述的变化发生在字节码中而不是源码中。
现在,当协程开始时,我们调用了它的 resumeWith()
—— label
是 0
,然后我们跳去 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<...> {
// 状态机当前状态
int label = 0
// 协程局部变量
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' 作为续体传递
if (result == COROUTINE_SUSPENDED) return // 如果 await 挂起了执行则返回
L1:
// 外部代码传入 .await() 的结果恢复协程
x += ((Integer) result).intValue()
label = -1
goto LOOP
END:
label = -1 // 没有其他步骤了
return
}
}
挂起函数代码在编译后的样子取决于它调用其他挂起函数的方式与时间。最简单的情况是一个挂起函数只在其末尾调用其他挂起函数,这称作对它们的尾调用。对于那些实现底层同步原语或者包装回调函数的协程来说,这是典型的方式,就像挂起函数小节与包装回调小节展示的那样。这些函数在末尾像调用 suspendCoroutine
那样调用其他挂起函数。编译这种挂起函数就与编译普通的非挂起函数一样,唯一的区别是通过 CPS 变换拿到的隐式续体参数会在尾调用中传递给下一个挂起函数。
如果挂起调用出现的位置不是末尾,编译器将为挂起函数生成一个状态机。状态机的实例在挂起函数调用时创建,在挂起函数完结时丢弃。
注意:在未来的版本中编译策略可能会优化成在第一个挂起点生成状态机实例。
反过来,不在尾部调用其他挂起函数时,这个状态机又充当了完结续体。挂起函数多次调用其他挂起函数时,状态机实例会被更新并重用。对比其他异步编程风格,(其他异步编程风格中)异步过程的每个后续步骤通常使用单独的、新分配的闭包来实现。
Kotlin 标准库提供了 kotlin.coroutines.intrinsics
包,其中包含许多声明,但应当谨慎使用,因为这些声明暴露了协程机制的内部实现细节。本节将解释这些细节。这些声明不应在通常的代码中使用,所以
kotlin.coroutines.intrinsics
包在 IDE 的自动补全中是被隐藏的。要使用这些声明,你必须手动把对应的 import 语句添加到源码文件:
import kotlin.coroutines.intrinsics.*
标准库中的 suspendCoroutine
挂起函数的实际实现使用了 Kotlin 本身来编写,其源代码作为标准库源码包的一部分是可见的。为了安全、无问题地使用协程,它在协程每次挂起时将状态机的实际续体包装在一个附加对象中。这对于异步计算以及 Future 等真正的异步用例来说非常好,因为相应异步原语的运行时开销远超分配一个额外的对象的开销。然而,对于生成器用例,这个额外的消耗过高,因此内建函数包为性能敏感的底层代码提供了原语。
标准库 kotlin.coroutines.intrinsics
包中名为 suspendCoroutineUninterceptedOrReturn
的函数拥有以下签名:
suspend fun <T> suspendCoroutineUninterceptedOrReturn(block: (Continuation<T>) -> Any?): T
它提供了对挂起函数的 CPS 的直接访问,并且暴露了对未拦截的续体的引用。后者意味着 Continuation.resumeWith
的调用可以不通过续体拦截器。它可以用于编写受限挂起的同步协程,因为这种协程不能安装续体拦截器(这又是因为它们的上下文始终为空),或者用在能确定当前执行线程就在所需的上下文中时(因为这时候也没必要拦截)。否则,应使用
intercepted
扩展函数(位于 kotlin.coroutines.intrinsics
包)获取被拦截的续体:
fun <T> Continuation<T>.intercepted(): Continuation<T>
并且还应该在被拦截到的续体上调用 Continuation.resumeWith
。
这时,如果协程确实挂起了,传递给 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
,它可用于同步协程以获得更好的性能。例如,下面是 sequence{}
构建器使用 createCoroutineUnintercepted
优化过的版本:
fun <T> sequence(block: suspend SequenceScope<T>.() -> Unit): Sequence<T> = Sequence {
SequenceCoroutine<T>().apply {
nextStep = block.createCoroutineUnintercepted(receiver = this, completion = this)
}
}
下面是 yield
使用 suspendCoroutineUninterceptedOrReturn
优化过的版本。注意,因为 yield
必定要挂起,对应的代码块也必定返回 COROUTINE_SUSPENDED
。
// 实现生成器
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 ->
// 找到或创建需要运行的代码块
startCoroutineUninterceptedOrReturn(completion = block) // 将结果返回到 suspendCoroutineUninterceptedOrReturn
}
这是非规范性的部分,不引入新的语言结构或库函数,而是讨论了一些涉及资源管理、并发以及编码风格的话题,并为各种各样的用例提供了更多示例。
协程不使用堆外存储,也不自行消耗任何本地资源,除非在协程中运行的代码打开了文件或占用了其他资源。在协程中打开的文件必然要以某种方式关闭,这不意味着协程本身需要关闭。当协程挂起时,其状态可以通过对其续体的引用来获取。如果你失去了对挂起协程续体的引用,最终它会被垃圾收集器回收。
打开了可关闭资源的协程应该特别关注。考虑下面这个受限挂起小节中使用 sequence{}
构建器从文件生成行序列的协程:
fun sequenceOfLines(fileName: String) = sequence<String> {
BufferedReader(FileReader(fileName)).use {
while (true) {
yield(it.readLine() ?: break)
}
}
}
这个函数返回一个 Sequence<String>
,通过这个函数,你可以用一种自然的方式打印文件的所有行:
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
函数没有机会结束调用并关闭文件。文件会一直处于开启状态,直到被垃圾收集器回收,因为 Java 文件有个能关闭文件的 finalizer
。如果只是个幻灯片或者短时间运行的小工具,这倒也不是什么问题,但是对于那些有数 GB 堆容量的大型后端系统来说,可就是个灾难了,它可能会快速耗尽打开文件句柄会而不是耗尽内存触发垃圾收集。
这个问题与 Java
中生成行的惰性流的 Files.lines
方法遇到的问题一样。它返回一个可关闭的 Java 流,但多数流操作不会自动调用对应的
stream.close
方法,需要用户自己记着关闭流。Kotlin 中也可以定义需要关闭的序列生成器,但也会遇到同一个问题,就是语言没有什么自动机制能保证它们在用完之后关闭。引入一种自动化资源管理的语言机制明显超出了 Kotlin 协程的领域。
然而,通常这个问题不会影响协程的异步用例。异步协程是不会被遗弃的,它会持续运行直到完毕。因此只要协程中的代码能正确地关闭其资源,资源最终就会被关闭。
在一个独立协程的内部,如同线程内部一样,是顺序执行的。这意味着下面这种协程内的代码是相当安全的:
launch { // 启动协程
val m = mutableMapOf<String, String>()
val v1 = someAsyncTask1() // 开始一些异步任务
val v2 = someAsyncTask2() // 开始一些异步任务
m["k1"] = v1.await() // 修改映射等待操作完成
m["k2"] = v2.await() // 修改映射等待操作完成
}
在协程的作用域里,你可以随意使用那些普通的单线程的可变结构。然而,在协程之间共享可变状态仍可能带来致命威胁。如果你使用了一个指定调度器的协程构建器,以 JS 风格在单一事件调度线程上恢复协程,就像续体拦截器一节展示的 Swing
拦截器那样,那你还是能安全地操作所有共享对象,因为它们总在事件调度线程上修改。但如果你在多线程环境中,或者需要运行在不同线程上的多个协程之间共享可变状态,你就必须使用线程安全(并发)的数据结构。
协程在这方面与线程没有什么不同,尽管协程确实更轻。你可以在仅仅几个线程上同时运行几百万个协程。一个运行着的协程总是在某个线程上。但一个挂起了的协程并不占用线程,也没有以任何方式绑定到线程。恢复协程的挂起函数通过在线程上调用 Continuation.resumeWith
决定在哪个线程上恢复协程。而协程的拦截器可以覆盖这个决定,并将协程的执行调度到另外的线程上。
异步编程有多种不同的风格。
异步计算一节已经讨论了回调函数,这通常也是协程被设计出来替换的最不方便的一种风格。任何回调风格的 API 都可以用对应的挂起函数包装,见这里。
我们来回顾一下。假设你现在有一个带有以下签名的阻塞 sendMail
函数:
fun sendEmail(emailArgs: EmailArgs): EmailResult
它在运行时可能会阻塞执行线程很长的时间。
要使其不阻塞,可以使用错误优先的 node.js 回调约定,以回调风格表示其非阻塞版本,签名如下:
fun sendEmail(emailArgs: EmailArgs, callback: (Throwable?, EmailResult?) -> Unit)
然而,协程还能支持其他风格的异步非阻塞编程。其中之一是内置于许多广受欢迎的语言中的 async/await 风格。在 Kotlin 中,可以通过引入 future{}
与 .await()
库函数来重现这种风格,就像用例中的 future 小节所示。
这种风格主张从函数返回对未来对象的某种约定,而不是传入回调函数作为参数。在这种异步风格中,sendEmail
的签名看起来是这样:
fun sendEmailAsync(emailArgs: EmailArgs): Future<EmailResult>
作为一种风格,将 Async 后缀添加到此类方法名称是一个好习惯,因为它们的参数与阻塞版本没什么不同,因而很容易犯忘记其本质是异步操作的错误。函数 sendEmailAsync
启动一个并发异步的操作,可能带来并发的所有陷阱。然而,鼓励这种风格的编程语言通常也提供某种 await
原语,在需要的时候把操作重新变回顺序的。
Kotlin 的原生编程风格基于挂起函数。在这种风格下,sendEmail
的签名看起来比较自然,不修改其参数或返回类型,而是增加了一个
suspend
修饰符:
suspend fun sendEmail(emailArgs: EmailArgs): EmailResult
我们已经发现,async 与挂起风格可以通过原语很容易地相互转换。例如,挂起版本的 sendEmail
可以用 future 构建器轻松实现 sendEmailAsync
:
fun sendEmailAsync(emailArgs: EmailArgs): Future<EmailResult> = future {
sendEmail(emailArgs)
}
sendEmailAsync
使用 .await()
挂起函数也能实现挂起函数 sendEmail
:
suspend fun sendEmail(emailArgs: EmailArgs): EmailResult =
sendEmailAsync(emailArgs).await()
因此,在某种意义上,这两种风格是等效的,并且在方便性上都明显优于回调风格。然而,我们还可以更深入地研究 sendEmailAsync
与挂起的 sendEmail
之间的区别。
让我们先比较一下他们在代码中使用的方式。挂起函数可以像普通函数一样使用:
suspend fun largerBusinessProcess() {
// 这里有很多代码,接下来在某处……
sendEmail(emailArgs)
// ……后来又继续做了些别的事
}
对应的异步风格函数这样使用:
fun largerBusinessProcessAsync() = future {
// 这里有很多代码,接下来在某处……
sendEmailAsync(emailArgs).await()
// ……后来又继续做了些别的事
}
显然,异步风格的函数结构更冗长,更容易出错。如果在异步风格的示例中省略了 .await()
调用,代码仍然可以编译并工作,但现在它通过发送电子邮件来处理异步,同时甚至在执行其余更大的业务流程,因此可能会修改某些共享状态并引入一些非常难以重现的错误。相反,挂起函数是默认顺序的。对于挂起的函数,无论何时需要任何并发,都可以在代码中通过调用某种 future{}
或类似的协程构建器显式地表达。
从使用多个库的大型项目的规模来比较这些风格。挂起函数是 Kotlin 的一个轻量级语言概念。所有挂起函数在任何非限定性的 Kotlin 协程中都是完全可用的。async 风格的函数依赖于框架。每个 promises/futures 框架都必须定义自己的——类async
函数,该函数返回自己的 promise/future 类,这些类又有对应的——类 async
函数。
从性能比较。挂起函数拥有最小的调用开销。你可以查看实现细节小节。除了必要的挂起机制之外,async 风格的函数需要额外维护相当重的 promise/future 抽象。async 风格的函数调用必须返回一些类似 future 的实例对象,并且即使函数非常简短,也无法将其优化。异步风格不太适合于粒度非常细的分解。
从与 JVM/JS 代码的互操作性比较。async 风格的函数与 JVM/JS 代码更具互操作性,因为这类代码的类型系统匹配 future 的抽象。在 Java 或 JS 中,它们只是返回类似 future 的对象的函数。对任何不原生支持
CPS 的语言来说,挂起函数都很奇怪。然而,从上面的示例中可以看出,对于任何给定的 promise/future 框架都很容易将任何挂起函数转换为
async 风格的函数。因此,只要用 Kotlin 编写一次挂起函数,然后使用适当的 future{}
协程构建器函数通过一行代码对其进行调整,就能实现与任何形式的 promise/future 的互操作性。
很多异步 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 包含了一个协作式可取消协程框架。它提供类似
suspendCoroutine
,但支持取消的suspendCancellableCoroutine
函数。查看其指南中取消与超时一文了解更多细节。
举一个更复杂的例子,我们看看异步计算用例中的 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<AsyncResult<T>>
回调。要通过协程简化任意的 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())
}
})
}
使用这个辅助函数,任意异步 vert.x 函数 async.foo(params, handler)
都可以在协程中这样调用:vx { async.foo(params, it) }
。
定义在 future 用例中类似于 launch{}
构建器的 future{}
构建器可以用于实现任何 future 或 promise 原语,这在协程构建器中做了一些介绍:
fun <T> future(context: CoroutineContext = CommonPool, block: suspend () -> T): CompletableFuture<T> =
CompletableFutureCoroutine<T>(context).also { block.startCoroutine(completion = it) }
它与 launch{}
的第一点不同是它返回
CompletableFuture
的实例,第二点不同是它包含一个默认为 CommonPool
的上下文,因此其默认执行在 ForkJoinPool.commonPool
,这个默认执行行为类似于
CompletableFuture.supplyAsync
方法。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
方法向协程报告结果。
协程不应使用 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
函数从其单独的“调度者”线程恢复协程。那些使用拦截器的协程,比如 Swing
,不会在这个线程上执行,因为它们的拦截器在合适的线程上调度它们。没有拦截器的协程会在调度者线程上调度。所以这对一个示例来说挺方便的,但它不是最有效的。最好能在相应的拦截器中实现原生的休眠。
对于 Swing
拦截器,非阻塞休眠的原生实现应使用专门为此目的设计的 Swing 计时器
:
suspend fun Swing.delay(millis: Int): Unit = suspendCoroutine { cont ->
Timer(millis) { cont.resume(Unit) }.apply {
isRepeats = false
start()
}
}
你可以从这里获取到这段代码。注意:kotlinx.coroutines 中的
delay
实现注意了拦截器特异性的休眠机制,并在适当的情况下自动使用上述方法。
在单线程应用中实现多任务非常方便,因为这样就不必处理并发或者共享可变状态了。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) // 休眠 1 秒
log("f1 returns 1")
1
}
val f2 = future(context) {
log("f2 is sleeping")
delay(1000) // 休眠 1 秒
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
开箱即用的实现。
如果你的整个应用都在同一个线程上执行,你可以定义自己的辅助协程构建器,在其中硬编码一个适应你单线程执行机制的上下文。
受限挂起小节展示的 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>
}
就像同步序列的作用域一样,我们也给它定义一个作用域接口,但它的挂起不是受限的:
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{}
上下文与非阻塞睡眠小节的非阻塞的 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) { // 等待生产者生产时挂起
// 在这里用值做些事,也可以在这里挂起
}
你可以在这里找到带有一些日志的示例,说明此处的执行情况,。
Go 风格的类型安全通道在 Kotlin 中通过库实现。我们可以为发送通道定义一个接口,包含挂起函数 send
:
interface SendChannel<T> {
suspend fun send(value: T)
fun close()
}
以及风格类似异步序列的接收通道,包含挂起函数 receive
与 operator iterator
:
interface ReceiveChannel<T> {
suspend fun receive(): T
suspend operator fun iterator(): ReceiveIterator<T>
}
Channel<T>
类同时实现这两个接口。通道缓存满时 send
挂起,通道缓存空时 receive
挂起。这样我们可以一字不差地复制 Go 风格的代码。Go 教程的第 4 个并发示例中向通道发送 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 代码主函数看起来是下面这样,其中的mainBlocking
是简化的辅助函数,它在 go{}
的线程池上调用 runBlocking
:
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 // 继续 while 循环
}
quit.onReceive {
println("quit")
false // 退出 while 循环
}
}
}
你可以在这里查看代码
示例具有 select {...}
的实现,它选择一种情况并返回结果,就像 Kotlin 的
when
表达式,还用到了一个方便的 whileSelect { ... }
,它就是 while(select<Boolean> { ... })
,但需要的括号比较少。
实现 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 // 继续循环
}
boom.onReceive {
println("BOOM!")
false // 继续循环
}
onDefault {
println(" .")
delay(50)
true // 继续循环
}
}
}
你可以在这里查看代码
这里的 Time.tick
与 Time.after
用非阻塞的 delay
函数实现非常简单。
这里能找到其他示例,注释里有对应的 Go 代码的链接。
注意,这是通道的简单实现,只用了一个锁来管理内部的等待队列。这使得它容易理解与解释。然而,它并不在这个锁下运行用户代码,因此它是完全并发的。这个锁只在一定程度上限制了它对大量并发线程的可伸缩性。
通道与
select
在 kotlinx.coroutines 中的实际实现基于无锁的无冲突并发访问数据结构。
这样实现的通道独立于协程上下文中的拦截器。它可以用于 UI 应用程序,通过续体拦截器小节提到的事件线程拦截器,或者任何别的拦截器,或者不使用任何拦截器也可以(在后一种情况下,实际的执行线程完全由协程中使用的其他挂起函数的代码决定)。通道实现提供的挂起函数都是非阻塞且线程安全的。
编写可伸缩的异步应用程序应遵循一个原则,确保代码挂起(使用挂起函数)而不阻塞,即实际上不阻塞线程。Java 并发原语
ReentrantLock
阻塞线程,不应在真正的非阻塞代码中使用。要控制对共享资源的访问,可以定义一个 Mutex
类,该类挂起协程的执行,而不是阻塞协程。这个类的声明看起来是这样:
class Mutex {
suspend fun lock()
fun unlock()
}
你可以从这里获得完整的实现。在 kotlinx.coroutines 中的实际实现还包含一些其他的函数。
使用这个非阻塞互斥的实现,Go 教程的第 9 个并发示例可以用 Kotlin 的 try finally
翻译到 Kotlin,这与 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 表达式传递给用实验性协程编译的库。在幕后,我们创建了对应的稳定版与实验性协程接口之间的适配器。
- 扩展阅读:
- 先读这个协程指南!。
- 介绍:
- 语言设计概述:
- 第 1 部分(原型设计):Kotlin 中的协程 (Andrey Breslav,于 JVMLS 2016)
- 第 2 部分(当前设计):Kotlin 协程新生 (Roman Elizarov,于 JVMLS 2017,幻灯片)
请将反馈提交到:
- Kotlin YouTrack 关于 Kotlin 编译器中协程的实现与特性的意见。
kotlinx.coroutines
关于支持库的意见。
本节概述了协程设计的诸次修订之间的变化。
- Coroutines are no longer experimental and had moved to
kotlin.coroutines
package.- The whole section on experimental status is removed and migration section is added.
- Some non-normative stylistic changes to reflect evolution of naming style.
- Specifications are updated for new features implemented in Kotlin 1.3:
- More operators and different types of functions are supports.
- Changes in the list of intrinsic functions:
suspendCoroutineOrReturn
is removed,suspendCoroutineUninterceptedOrReturn
is provided instead.createCoroutineUnchecked
is removed,createCoroutineUnintercepted
is provided instead.startCoroutineUninterceptedOrReturn
is provided.intercepted
extension function is added.- Moved non-normative sections with advanced topics and more examples to the appendix at end of the document to simplify reading.
- Added description of
createCoroutineUnchecked
intrinsic.
This revision is implemented in Kotlin 1.1.0 release.
kotlin.coroutines
package is replaced withkotlin.coroutines.experimental
.SUSPENDED_MARKER
is renamed toCOROUTINE_SUSPENDED
.- Clarification on experimental status of coroutines added.
This revision is implemented in Kotlin 1.1-Beta.
- Suspending functions can invoke other suspending function at arbitrary points.
- Coroutine dispatchers are generalized to coroutine contexts:
CoroutineContext
interface is introduced.ContinuationDispatcher
interface is replaced withContinuationInterceptor
.createCoroutine
/startCoroutine
parameterdispatcher
is removed.Continuation
interface includesval context: CoroutineContext
.CoroutineIntrinsics
object is replaced withkotlin.coroutines.intrinsics
package.
This revision is implemented in Kotlin 1.1-M04.
- The
coroutine
keyword is replaced by suspending functional type.Continuation
for suspending functions is implicit both on call site and on declaration site.suspendContinuation
is provided to capture continuation is suspending functions when needed.- Continuation passing style transformation has provision to prevent stack growth on non-suspending invocations.
createCoroutine
/startCoroutine
coroutine builders are introduced.- The concept of coroutine controller is dropped:
- Coroutine completion result is delivered via
Continuation
interface.- Coroutine scope is optionally available via coroutine
receiver
.- Suspending functions can be defined at top-level without receiver.
CoroutineIntrinsics
object contains low-level primitives for cases where performance is more important than safety.
原文 | 译文 | 语境 |
---|---|---|
complete / completion | 完结 | 主语是协程,或协程包装的东西 |
continuation | 续体 | 所有 |
continuation passing style | 续体传递风格 | 所有 |
coroutine intrinsics | 协程内建函数 | 所有 |