你的第一个 Kotlin 协程程序
作者 | Dmitry Jemerov,乔禹昂(翻译) |
在 Kotlin 1.1 中引入的协程,一种全新的编写异步、非阻塞(以及更多)代码的方式。在这篇教程中我们将通过一些使用 Kotlin 协程的基础示例来帮助我们学习 kotlinx.coroutines
库,它是现有 Java 库的帮助程序和包装器的集合。
创建一个工程
Gradle
在 IntelliJ IDEA 中依次点击 File -> New > Project…:
接下来跟随向导的步伐。你将根据这篇文档配置使用启用 Kotlin 的 build.gradle
文件。
确保它配置了 Kotlin 1.3 或者更高版本。
由于我们将使用 kotlinx.coroutines
,来让我们将它最近的版本添加到依赖中:
dependencies {
...
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
}
dependencies {
...
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2")
}
这个库已经发布到了 Bintray JCenter 仓库,所以让我们添加它:
repositories {
jcenter()
}
就是这样,我们很高兴去 src/main/kotlin
路径下编写代码。
Maven
在 IntelliJ IDEA 中依次点击 File -> New > Project… 并检查 Create from archetype 框:
接下来跟随向导的步伐。你将根据这篇文档配置使用启用 Kotlin 的 pom.xml
文件。
确保它配置了 Kotlin 1.3 或者更高版本。
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
...
<configuration>
<args>
<arg>-Xcoroutines=enable</arg>
</args>
</configuration>
</plugin>
由于我们将使用 kotlinx.coroutines
,来让我们将它最近的版本添加到依赖中:
<dependencies>
...
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>1.4.2</version>
</dependency>
</dependencies>
这个库已经发布到了 Bintray JCenter 仓库,所以让我们添加它:
<repositories>
...
<repository>
<id>central</id>
<url>https://jcenter.bintray.com</url>
</repository>
</repositories>
就是这样,我们很高兴去 src/main/kotlin
路径下编写代码。
我的第一个协程
人们可以将协程视为轻量级线程。就像线程一样,协程可以并行运行,等待其它协程以及通信。 最大的不同是协程是非常廉价的,几乎免费的:我们可以创建上千个协程,并且在性能的消耗上非常之低。 在另一方面,启动真正的线程并保持它们运行的代价是非常昂贵的。一千个线程对于现代机器来说可能是一个严峻的挑战。
所以,我们如何才能启动一个协程?让我们使用 launch {}
函数:
launch {
...
}
这里启动了一个新的协程。默认的,协程运行在一个共享的线程池中。 线程仍然存在于基于协程的程序中,但是一个线程可以运行大量的协程,所以这里不需要太多线程。
让我们看看使用 launch
函数的完整程序:
import kotlinx.coroutines.*
fun main(args: Array<String>) {
//sampleStart
println("Start")
// 启动一个协程
GlobalScope.launch {
delay(1000)
println("Hello")
}
Thread.sleep(2000) // 等待 2 秒钟
println("Stop")
//sampleEnd
}
这里我们启动了一个协程并等待 1 秒钟以及打印 Hello
。
我们使用了类似 Thread.sleep()
的 delay()
函数,但是它更优异:它 不会阻塞一个线程 ,但是会挂起协程自身。
当这个协程处于等待状态时该线程会返回线程池中,当等待结束的时候,这个协程会在线程池中的空闲线程上恢复。
主线程(通过 main()
函数运行的线程)必须等到我们的协程完成,否则程序会在 Hello
被打印之前终止。
练习:尝试从上面的程序中删除 sleep()
并查看结果。
如果我们直接在 main()
中尝试使用诸如 delay()
这样的非阻塞函数,我们将得到一个编译错误:
挂起函数只被允许在协程或另一个挂起函数中调用
这是因为我们不在任何协程中。我们可以在 runBlocking {}
包装中使用 delay,它启动了一个协程并等待直到它结束:
runBlocking {
delay(2000)
}
所以,程序首先打印 Start
,接下来通过 launch {}
来运行一个协程,再接下来通过 runBlocking {}
运行另一个协程并阻塞直至它结束,然后打印 Stop
。与此同时第一个协程执行完毕并打印 Hello
。 我们告诉过你,就像线程一样 :)
让我们大量创建它们
现在,让我们来确认协程真的比线程更廉价。那启动它们一百万次会怎样?让我们先来启动一百万个线程:
val c = AtomicLong()
for (i in 1..1_000_000L)
thread(start = true) {
c.addAndGet(i)
}
println(c.get())
这里运行了 1'000'000 个线程并为每个都增加了一个共同的计数器。在我的机器上执行完这个程序之前,我的耐心已经耗尽(绝对超过一分钟)。
让我们尝试使用协程来做相同的事:
val c = AtomicLong()
for (i in 1..1_000_000L)
GlobalScope.launch {
c.addAndGet(i)
}
println(c.get())
这个示例在不到一秒的时间内就完成了,但它打印一些任意数字,因为一些协程没有在 main()
打印结果之前执行完毕。让我们来修正它。
我们可以使用一些同样适用于线程的同步方法(在这种情况下,一个 CountDownLatch
就是我的想法),但是,让我们走一条更安全,更简洁的道路。
异步:从协程中返回一个值
另一个启动协程的方法是 async {}
。它类似于 launch {}
,但返回一个 Deferred<T>
实例,它拥有一个 await()
函数来返回协程执行的结果。Deferred<T>
是一个非常基础的 future(还支持完全成熟的 JDK future,但是现在我们将局限于我们自己的 Deferred
)。
让我们再次创建一百万个协程,并保持它们的 Deferred
对象的引用。现在这里不再需要原子计数,我们可以仅仅返回从协程中添加的数字:
val deferred = (1..1_000_000).map { n ->
GlobalScope.async {
n
}
}
所有这些都已经启动,我们所需要的只是收集结果:
val sum = deferred.sumOf { it.await().toLong() }
这里我们简单从每个协程等待并取得它的执行结果,接下来将使用标准库的 sumOf()
函数来将所有结果叠加到一起。但编译器理所当然地抱怨道:
挂起函数只被允许在协程或另一个挂起函数中调用
await()
不能在协程外调用,因为它需要挂起直至计算结束,并且只有协程可以被无阻塞的挂起。因此,让我们将它们放到协程中:
runBlocking {
val sum = deferred.sumOf { it.await().toLong() }
println("Sum: $sum")
}
现在它打印了一些合理的东西:500000500000
,因为所有的协程都执行完毕了。
让我们也确保我们的协程是实际并行运行的。如果我们在每个 async
中添加了 1 秒钟的 delay()
,程序将不会运行 1'000'000 秒(超过 11.5 天):
val deferred = (1..1_000_000).map { n ->
GlobalScope.async {
delay(1000)
n
}
}
这在我的机器上花费了 10 秒,所以,协程是并行的。
挂起函数
现在,假设我们要将 工作 (“等待 1 秒并返回一个数字”)提取到一个单独的函数中:
fun workload(n: Int): Int {
delay(1000)
return n
}
弹出一个熟悉的错误:
挂起函数只被允许在协程或另一个挂起函数中调用
让我们深入了解它的含义。协程的最大优点是它们可以 挂起 而不会阻塞一个线程。编译器必须发出一些特殊代码才能实现这一点,所以在这段代码中我们需要显式地将函数标记为 可挂起 。我们对它使用了 suspend
修饰符:
suspend fun workload(n: Int): Int {
delay(1000)
return n
}
现在当我们从协程中调用 workload()
,编译器知道它可以挂起并相应地准备:
GlobalScope.async {
workload(n)
}
我们的 workload()
可以在一个协程中调用(或另一个挂起函数),但是 不能 在协程外调用。自然地,delay()
与 await()
这些我们在上面使用的函数它们自己也被修饰为 suspend
,并且这也是为什么我们要将它们放入 runBlocking {}
,launch {}
或者 async {}
中。