Kotlin作为一种语言,在其标准库中仅提供最小的低级API,以使各种其他库能够使用协程。与许多具有类似功能的其他语言一样,async和await不是Kotlin中的关键字,甚至不是其标准库的一部分。此外,Kotlin的暂停函数概念为异步操作提供了比 futures 和 promises更安全且更不易出错的抽象。kotlinx.coroutines
是由JetBrains开发的丰富的协程库。它包含本指南涵盖的许多高级协程启用的原函数,包括启动,异步等
这是关于kotlinx.coroutines
的核心功能的指南,其中包含一系列示例,分为不同的主题。
为了使用协程以及遵循本指南中的示例,您需要像kotlinx.coroutines/README.md中解释的那样在kotlinx-coroutines-core
模块中添加依赖项。
/*
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.basic01
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch { // launch new coroutine in background and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello,") // main thread continues while coroutine is delayed
Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}
代码出处:kotlinx.coroutines/example-basic-01.kt at master · Kotlin/kotlinx.coroutines
运行结果:
Hello,
World!
从本质上讲,协同程序是轻量级的线程。它们是在CoroutineScope
(协程器)的上下文中与启动协程构建器一起创建的。在这里,我们将在GlobalScope
(全局协程器)中启动一个新的协程,这意味着新协程的生命周期仅受整个应用程序的生命周期的限制。
您可以使用Thread.sleep(...)
替换GlobalScope.launch {...}
和Thread.sleep{...}
以及delay(...)
。试试吧。
如果您首先将Thread
替换GlobalScope.launch
,编译器将生成以下错误:
Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function
这是因为delay
是一个特殊的挂起函数
,它不会阻塞一个线程(Thread
),但会暂停coroutine
,它只能在一个协程中使用。
第一个示例在同一代码中混合非阻塞delay(...)
和阻塞Thread.sleep(...)
。很容易忘记哪一个阻塞,哪一个没有阻塞。让我们明确一下使用runBlocking
协程构建器进行阻塞:
/*
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.basic02
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch { // launch new coroutine in background and continue
delay(1000L)
println("World!")
}
println("Hello,") // main thread continues here immediately
runBlocking { // but this expression blocks the main thread
delay(2000L) // ... while we delay for 2 seconds to keep JVM alive
}
}
代码出处:kotlinx.coroutines/example-basic-02.kt at master · Kotlin/kotlinx.coroutines
运行结果:
Hello,
World!
结果是相同的,但此代码仅使用非阻塞delay
。调用runBlocking
的主线程阻塞,直到runBlocking
内的协程完成。
这个例子也可以用更惯用的方式重写,使用runBlocking
来包main
函数的执行:
/*
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.basic02b
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> { // start main coroutine
GlobalScope.launch { // launch new coroutine in background and continue
delay(1000L)
println("World!")
}
println("Hello,") // main coroutine continues here immediately
delay(2000L) // delaying for 2 seconds to keep JVM alive
}
代码出处: kotlinx.coroutines/example-basic-02b.kt at master · Kotlin/kotlinx.coroutines
运行结果:
Hello,
World!
这里runBlocking
作为适配器,用于启动顶级主协程。我们明确指定了它的Unit返回类型,因为Kotlin中格式良好的main
函数必须返回Unit
这也是一种为挂起函数编写单元测试的方法:
class MyTest {
@Test
fun testMySuspendingFunction() = runBlocking<Unit> {
// here we can use suspending functions using any assertion style that we like
}
}
在另一个协程正在工作时延迟一段时间并不是一个好方法。让我们明确等待(以非阻塞方式),直到我们启动的后台作业完成:
/*
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.basic03
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val job = GlobalScope.launch { // launch new coroutine and keep a reference to its Job
delay(1000L)
println("World!")
}
println("Hello,")
job.join() // wait until child coroutine completes
//sampleEnd
}
代码出处: kotlinx.coroutines/example-basic-03.kt at master · Kotlin/kotlinx.coroutines
运行结果:
Hello,
World!
现在结果仍然相同,但主协程的代码不以任何方式与后台作业的持续时间相关联。好多了。
对于协程的实际使用仍有一些期望。当我们使用GlobalScope.launch
时,我们创建了一个顶级协程
。尽管它很轻
,但它在运行时仍会消耗一些内存资源。如果我们忘记保留对新启动的协程的引用,它仍会运行。如果协程中的代码挂起(例如,我们错误地延迟了太长时间),如果我们启动了太多的协程并且内存不足会怎么样?必须手动保持对所有已启动的协程的引用并join
它们是容易出错的.
有一个更好的解决方案。我们可以在代码中使用结构化并发。就像我们通常使用线程
(线程总是全局的)一样,我们可以在我们正在执行的操作的特定范围内启动协程,而不是在GlobalScope
中启动协程。
在我们的示例中,我们使用runBlocking
协程构建器将main
函数转换为协程。每个协程构建器(包括runBlocking
)都将CoroutineScope
的实例添加到其代码块的范围内。我们可以在此范围内启动协程,而无需显式join
它们,因为在其范围内启动的所有协程完成之前,外部协程(在我们的示例中为runBlocking
)不会完成。
/*
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.basic03s
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch { // launch new coroutine in the scope of runBlocking
delay(1000L)
println("World!")
}
println("Hello,")
}
代码出处: kotlinx.coroutines/example-basic-03s.kt at master · Kotlin/kotlinx.coroutines
运行结果:
Hello,
World!
除了由不同构建器提供的协同作用域之外,还可以使用coroutineScope
构建器声明自己的作用域。它会创建新的协程范围,并且在所有已启动的子项完成之前不会完成。 runBlocking
和coroutineScope
之间的主要区别在于后者在等待所有子项完成时不会阻塞当前线程。
/*
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.basic04
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch {
delay(200L)
println("Task from runBlocking")
}
coroutineScope { // Creates a new coroutine scope
launch {
delay(500L)
println("Task from nested launch")
}
delay(100L)
println("Task from coroutine scope") // This line will be printed before nested launch
}
println("Coroutine scope is over") // This line is not printed until nested launch completes
}
代码出处: kotlinx.coroutines/example-basic-04.kt at master · Kotlin/kotlinx.coroutines
运行结果:
Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over
让我们将launch {...}
中的代码块提取到一个单独的函数中。当您对此代码执行“提取功能”重构时,您将获得一个带有suspend
修饰符的新函数。这是你的第一个挂起函数
。挂起函数
可以在协程内部使用,就像常规函数一样,但它们的附加功能是它们可以反过来使用其他挂起函数(如本例中的delay
)来暂停执行协程。
/*
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.basic05
import kotlinx.coroutines.*
fun main() = runBlocking {
launch { doWorld() }
println("Hello,")
}
// this is your first suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}
代码出处:kotlinx.coroutines/example-basic-05.kt at master · Kotlin/kotlinx.coroutines
运行结果:
Hello,
World!
但是,如果提取的函数包含了在当前作用域上调用的协程构建器,该怎么办?在这种情况下,提取函数上的suspend
修饰符是不够的。在CoroutineScope
上做一个doWorld
扩展方法是其中一种解决方案,但它并不总是适用,因为它不会使API更清晰。惯用解决方案是将显式CoroutineScope
作为包含目标函数的类中的字段,或者在外部类实现CoroutineScope
时隐式。作为最后的手段,可以使用CoroutineScope(coroutineContext)
,但是这种方法在结构上是不安全的,因为您不再能够控制此方法的执行范围。只有私有API才能使用此构建器。
运行以下代码:
import kotlinx.coroutines.*
fun main() = runBlocking {
repeat(100_000) { // launch a lot of coroutines
launch {
delay(1000L)
print(".")
}
}
}
代码出处: kotlinx.coroutines/example-basic-06.kt at master · Kotlin/kotlinx.coroutines
运行结果: 省略(请使用IntelliJ IDEA自行测试)
它启动了10万个协程,一秒钟之后,每个协程都打印出一个点。现在,尝试使用线程。会发生什么? (很可能你的代码会产生某种内存不足的错误)
下面的代码在GlobalScope
中启动一个长时间运行的协程,它打印“我正在睡觉”
每秒两次,然后在一段延迟后从主函数返回:
/*
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.basic07
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
GlobalScope.launch {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L) // just quit after delay
//sampleEnd
}
代码出处: kotlinx.coroutines/example-basic-07.kt at master · Kotlin/kotlinx.coroutines
运行结果:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
省略(请使用IntelliJ IDEA自行测试)
在GlobalScope
中启动的活动协程不会使进程保持活动状态。它们就像守护线程
。
这一部分包含了协程的取消与超时。
*`取消与超时](#取消与超时)
在一个长时间运行的应用程序中,你也许需要对你的后台协程进行细粒度的控制。
比如说,一个用户也许关闭了一个启动了协程的界面,那么现在协程的执行结果
已经不再被需要了,这时,它应该是可以被取消的。
该launch 函数返回了一个可以被用来取消运行中的协程的Job:
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L) // 延迟一段时间
println("main: I'm tired of waiting!")
job.cancel() // 取消该任务
job.join() // 等待任务执行结束
println("main: Now I can quit.")
}
代码出处: (https://github.com/hltj/kotlinx.coroutines-cn/blob/master/kotlinx-coroutines-core/jvm/test/guide/example-cancel-01.kt)
程序执行后的输出如下:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
一旦 main 函数调用了job.cancel,我们在其它的协程中就看不到任何输出,因为它被取消了。
这里也有一个可以使Job 挂起的函数cancelAndJoin
它合并了对cancel
Job.cancel 以及join
Job.join 的调用。
协程的取消是 协作 的。一段协程代码必须协作才能被取消。
所有 kotlinx.coroutines
中的挂起函数都是 可被取消的 。它们检查协程的取消,
并在取消时抛出CancellationException。 然而,如果协程正在执行
计算任务,并且没有检查取消的话,那么它是不能被取消的,就如如下示例
代码所示:
import kotlinx.coroutines.*
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
// 每秒打印消息两次
if (System.currentTimeMillis() >= nextPrintTime) {
println("I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 等待一段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消一个任务并且等待它结束
println("main: Now I can quit.")
}
代码出处: (https://github.com/hltj/kotlinx.coroutines-cn/blob/master/kotlinx-coroutines-core/jvm/test/guide/example-cancel-02.kt)
运行示例代码,并且我们可以看到它连续打印出了“I’m sleeping” ,甚至在调用取消后,
任务仍然执行了五次循环迭代并运行到了它结束为止。
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
I'm sleeping 3 ...
I'm sleeping 4 ...
main: Now I can quit.
我们有两种方法来使执行计算的代码可以被取消。第一种方法是定期
调用挂起函数来检查取消。对于这种目的yield
是一个好的选择。
另一种方法是显式的检查取消状态。让我们试试第二种方法。
将前一个示例中的 while (i < 5)
替换为 while (isActive)
并重新运行它。
import kotlinx.coroutines.*
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) { // 可以被取消的计算循环
// 每秒打印消息两次
if (System.currentTimeMillis() >= nextPrintTime) {
println("I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 等待一段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消该任务并等待它结束
println("main: Now I can quit.")
}
代码出处: (https://github.com/hltj/kotlinx.coroutines-cn/blob/master/kotlinx-coroutines-core/jvm/test/guide/example-cancel-03.kt)
你可以看到,现在循环被取消了。isActive
是一个可以被使用在
CoroutineScope 中的扩展属性。
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
我们通常使用如下的方法处理在被取消时抛出CancellationException 的可被取消
的挂起函数。比如说,try {……} finally {……}
表达式以及 Kotlin 的 use
函数一般在协程被取消的时候
执行它们的终结动作:
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
} finally {
println("I'm running finally")
}
}
delay(1300L) // 延迟一段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消该任务并且等待它结束
println("main: Now I can quit.")
}
代码出处: (https://github.com/hltj/kotlinx.coroutines-cn/blob/master/kotlinx-coroutines-core/jvm/test/guide/example-cancel-04.kt)
join 和cancelAndJoin 等待了所有的终结动作执行完毕,
所以运行示例得到了下面的输出:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
I'm running finally
main: Now I can quit.
在前一个例子中任何尝试在 finally
块中调用挂起函数的行为都会抛出
CancellationException,因为这里持续运行的代码是可以被取消的。通常,这并不是一个
问题,所有良好的关闭操作(关闭一个文件、取消一个任务、或是关闭任何一种
通信通道)通常都是非阻塞的,并且不会调用任何挂起函数。然而,在
真实的案例中,当你需要挂起一个被取消的协程,你可以将相应的代码包装在
withContext(NonCancellable) {……}
中,并使用withContext 函数以及NonCancellable 上下文,见如下示例所示:
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
} finally {
withContext(NonCancellable) {
println("I'm running finally")
delay(1000L)
println("And I've just delayed for 1 sec because I'm non-cancellable")
}
}
}
delay(1300L) // 延迟一段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消该任务并等待它结束
println("main: Now I can quit.")
}
代码出处: (https://github.com/hltj/kotlinx.coroutines-cn/blob/master/kotlinx-coroutines-core/jvm/test/guide/example-cancel-05.kt)
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
I'm running finally
And I've just delayed for 1 sec because I'm non-cancellable
main: Now I can quit.
在实践中绝大多数取消一个协程的理由是
它有可能超时。
当你手动追踪一个相关Job 的引用并启动了一个单独的协程在
延迟后取消追踪,这里已经准备好使用withTimeout 函数来做这件事。
来看看示例代码:
import kotlinx.coroutines.*
fun main() = runBlocking {
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
}
代码出处: (https://github.com/hltj/kotlinx.coroutines-cn/blob/master/kotlinx-coroutines-core/jvm/test/guide/example-cancel-06.kt)
运行后得到如下输出:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
withTimeout 抛出了 TimeoutCancellationException
,它是CancellationException的子类。
我们之前没有在控制台上看到堆栈跟踪信息的打印。这是因为
在被取消的协程中 CancellationException 被认为是协程执行结束的正常原因。
然而,在这个示例中我们在 main
函数中正确地使用了 withTimeout。
由于取消只是一个例外,所有的资源都使用常用的方法来关闭。
如果你需要做一些各类使用超时的特别的额外操作,可以使用类似withTimeout
的withTimeoutOrNull函数,并把这些会超时的代码包装在 try {...} catch (e: TimeoutCancellationException) {...}
代码块中,而withTimeoutOrNull 通过返回 null
来进行超时操作,从而替代抛出一个异常:
import kotlinx.coroutines.*
fun main() = runBlocking {
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
"Done" // 在它运行得到结果之前取消它
}
println("Result is $result")
}
代码出处: (https://github.com/hltj/kotlinx.coroutines-cn/blob/master/kotlinx-coroutines-core/jvm/test/guide/example-cancel-07.kt)
运行这段代码时不再抛出异常:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null