文档翻译-Kotlin协程 基本用法

该文档由作者自己的理解翻译,若有出入,敬请谅解。

Kotlin协程的基本用法

这一篇我们学习协同程序的基本概念。查看原文

你的第一个协同程序

复制下面的代码到你的程序中并运行:

1
2
3
4
5
6
7
8
9
10
import kotlinx.coroutines.*

fun main() {
GlobalScope.launch { // 启动一个新的后台协同程序,并继续
delay(1000L) // 非阻塞的延时1秒(默认时间单位是毫秒)
println("World!") // 延时结束打印
}
println("Hello,") // 协同程序结束延时后,主线程继续执行
Thread.sleep(2000L) // 阻塞主线程2秒用来保持虚拟机运行中
}

查看全部代码

你将看到如下结果:

1
2
Hello,
World!

本质上,协同程序是轻量级线程。它们被启动在一些协同程序范围的上下文。现在我们启动一个新的协同程序在全局范围,这意味着这个新的协同程序的生命周期被整个应用的生命周期所限制。

替换成 GlobalScope.launch { ... }thread { ... }delay(...)Thread.sleep(...), 你将能够得到同样的结果。试试吧(别忘记引入kotlin.concurrent.thread)。

如果你用thread替换GlobalScope.launch,编译器将出现以下错误:

1
Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function

那是因为delay是一个特殊的挂起方法不会阻塞线程,但是挂起协同程序和它都只能在协同程序中使用。

桥接阻塞和非阻塞世界

第一个例子将非阻塞的delay{} 和阻塞的Thread.sleep()混合在同一段代码中。这将很容易的忘记哪个是阻塞的哪个是非阻塞的。让我们使用runBlocking协程创建者明确关于阻塞:

1
2
3
4
5
6
7
8
9
10
11
12
import kotlinx.coroutines.*

fun main() {
GlobalScope.launch { // 启动一个新的后台协同程序,并继续
delay(1000L)
println("World!")
}
println("Hello,") // 主线程将立即执行
runBlocking { // 但是这个代码块阻塞在主线程
delay(2000L) // ... 我们延时2秒保持虚拟机的运行
}
}

查看全部代码

结果是一样的,但是这次的代码仅仅使用的是非阻塞的delay。在主线程唤起 runBlocking 阻塞直到runBlocking内的协程执行完成。

这个举例也能够使用更加通用的方式来重写,使用runBlocking去包裹主方法的执行:

1
2
3
4
5
6
7
8
9
10
import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { // 开始主协程
GlobalScope.launch { // 启动一个后台的协程
delay(1000L)
println("World!")
}
println("Hello,") // 主协程立即执行
delay(2000L) // 延时2秒保持虚拟机的运行
}

查看全部代码

这里的runBlocking { … }作为一个被用于开始最高级的主协程(我们可以使用runBlocking开启一个最高等级的主协程)。我们明确指定它的返回类型Unit,因为一个好的主方法结构必须要返回Unit
这也是写挂起方法的单元测试的一种方式。

1
2
3
4
5
6
class MyTest {
@Test
fun testMySuspendingFunction() = runBlocking<Unit> {
// 在这里我们可以使用我们喜欢的任何风格的挂起方法
}
}

等待任务

当其他协程正在执行时,我们等待一段时间并不是一个好的方式。让我们明确的等待直到我们开启的后台工作完成。

1
2
3
4
5
6
7
8
9
10
11
12
import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
val job = GlobalScope.launch { // launch a new coroutine and keep a reference to its Job
delay(1000L)
println("World!")
}
println("Hello,")
job.join() // wait until child coroutine completes
//sampleEnd
}

查看完整代码
现在的结果仍然是一样的,但是主协程的代码不依赖于任何后台工作的时长。更加好。

结构并发

我们仍然有一些期望更加实用的协程。当我们使用GlobalScope.launch,我们创建 一个顶级的协程。尽管这是轻量级的,它运行时仍然会消耗一些内存资源。如果我们忘了给它指向新的引用,那么它会一直运行。如果代码在协程中挂起(例如,我们错误的延时很长时间),如果我们开启了太多的协程导致超过内存限制会怎么样?不得不手动的保持所有开启的协程引用,链接他们是错误的想发(倾向)。

有一个更好的解决办法。我们可以使用并发结构的代码。为了如我们使用线程(线程都是全局的)一样的在全局启动协程,我们可以在一个我们能够控制的一个域中启动协程。

回到我们的例子,我们由main方法已经被转变成使用runBlocking创建的协程。所有的协程创建方式,包括runBlocking,在代码块中添加了一个协程域的实例。我们能够在这个域中开启一个协程并且明确的没有join到域中,因为外层协程不会执行完成知道它的域开启的所有协程都完成。所以,我也可以类似的改造我们的例子:

1
2
3
4
5
6
7
8
9
import kotlinx.coroutines.*

fun main() = runBlocking { // 当前的协程域
launch { //在runVloacking下开启了一个新的协程
delay(1000L)
println("World!")
}
println("Hello,")
}

查看所有代码

域的创建

除了不同创建者提供的协程域,你也可以使用coroutineScope创建声明自己的域。它创建的一个协程域不会完成直到所有开启的子协程完成。
runBlockingcoroutineScope可能看起来比较相似,因为他们都是等待他们内部和所有子协程完成。这两个的主要不同是,runBlocking方法阻塞了当前线程去等待,而coroutineScope仅仅是挂起,释放下面的线程给其他用。由于上述的不同,runBlocking是一个普通的方法而coroutineScope是一个挂起的方法。
下面的例子演示一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
launch {
delay(200L)
println("Task from runBlocking")
}

coroutineScope { // 创建一个协程域
launch {
delay(500L)
println("Task from nested launch")
}

delay(100L)
println("Task from coroutine scope") // 这一行将在延时之前执行
}

println("Coroutine scope is over") // 这一行不会执行直到延时的代码执行完毕
}

查看所有代码
从结果注意到当延时任务执行等待时,”Task from coroutine scope”就已经执行了, “Task from runBlocking”也会执行和打印,尽管coroutineScope还没有执行完成。

提取方法重构

让我们提取代码块中的launch{}成一个独立的方法。当你把代码提取出来成一个新的方法,需要加上suspend修饰。这就是你的第一个挂起的方法。挂起方法和普通方法一样能够在协程中被使用,但是额外的特点是他们能够有序、使用其他挂起的方法,就像这个例子中的delay{},在协程中挂起执行。

1
2
3
4
5
6
7
8
9
10
11
12
import kotlinx.coroutines.*

fun main() = runBlocking {
launch { doWorld() }
println("Hello,")
}

// this is your first suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}

查看完整代码
但是如果提取的方法中包含协程的被执行的创建者在当前的域中会怎么样?这种情况下仅仅在提取的方法上添加suspend修饰符是不够的。在CoroutineScope写一个扩展的方法是一个解决办法,但是它可能不总是合适的由于它没有清理的API。管用的方法是在一个包含目标方法的类中显式的有一个CoroutineScope域或者外部实现CoroutineScope的类有隐式的域。作为最后的手段,使用CoroutineScope(coroutineContext),但是这种方法在结构上是不安全的,因为你在这个域中没有了执行方法的控制权。仅私有API能够使用这个创建者。

协程是轻量级

执行下面的代码:

1
2
3
4
5
6
7
8
9
10
import kotlinx.coroutines.*

fun main() = runBlocking {
repeat(100_000) { // 开启很多协程
launch {
delay(1000L)
print(".")
}
}
}

查看完整代码
开启十万个协程,一秒后分别打印一个点。然后尝试使用线程做这件事。会发生什么?(很有可能会产生内存不足的错误)。

全局的协程就像是后台守护线程

下面的代码开启了一个长时间运行的协程在全局域,它每一秒打印I'm sleeping然后等待一些时间后从主方法中退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
GlobalScope.launch {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L) // 延时后退出
//sampleEnd
}

查看完整代码
你运行后可以看到以下三行打印然后结束:

1
2
3
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...

我们在全局域启动的活跃协程不会保持进程的存活。他们就像是守护线程。

分享到