协程基础
这部分讲述协程基础概念。
第一个协程
运行如下代码:
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch { // 在后台启动新协程,然后继续运行
delay(1000L) // 非阻塞式延时1秒(默认时间单位是毫秒)
println("World!") // 在延时后打印
}
println("Hello,") // 当协程延时时主线程继续
Thread.sleep(2000L) // 为了保活JVM,阻塞主线程两秒
}
在这里获取完整代码
结果如下:
Hello,
World!
本质上,协程是轻量级线程。它们由launch 协程生成器(coroutine builder) 在某个CoroutineScope的环境下启动。 这里,我们在GlobalScope中启动了一个新的协程,意思是,新协程的生命周期仅仅由整个应用的生命周期限制。
用thread { … } 替代GlobalScope.launch { … }、delay(…) 替代 Thread.sleep(…),你可以达到相同的效果。快试试吧!
如果你thread替代GlobalScope.launch,编译器产生如下错误:
Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function
这是因为,delay是特殊的挂起函数,它不会阻塞一个线程,但是会挂起协程,而且它只能在一个协程中使用。
桥接阻塞式和非阻塞式的情景
第一个例子在相同代码中混合了非阻塞式delay(…) 和阻塞式Thread.sleep(…)。对于哪个是阻塞式和哪个不是,容易失去线索。让我们使用runBlocking协程生成器显示地来说明阻塞式:
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch { //在后台启动新协程,然后继续运行
delay(1000L)
println("World!")
}
println("Hello,") // 主线程立即运行到这里
runBlocking { // 但是下面表达式阻塞了主线程
delay(2000L) // ... 为了保活JVM,延迟了两秒
}
在这里获取完整代码
结果是一样的,但是这个代码仅仅用了非阻塞式delay。在主线程,调用了runBlocking,阻塞直到runBlocking里面的协程结束。
这个例子也可以用更加惯用的方式重新编写,即使用runBlocking包裹主函数的执行:
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> { // 开启主协程
GlobalScope.launch { // 在后台启动新协程,然后继续运行
delay(1000L)
println("World!")
}
println("Hello,") // 主线程立即运行到这里
delay(2000L) // 为了保活JVM,延迟了两秒
}
在这里获取完整代码
这里runBlocking<Unit> { … } 作为一个适配器使用,这个适配器用来开启顶层主协程。显式地指定它的Unit返回类型,这是因为在kotlin里面,一个符合语法规则的main函数不得不返回Unit。
如下也是为挂起函数编写单元测试的一种方式:
class MyTest {
@Test
fun testMySuspendingFunction() = runBlocking<Unit> {
// 通过使用断言样式,这里可以使用挂起函数
}
}
等待一个job
在另外一个协程运行时延迟一段时间,这不是一个好方法。让我们显式地等待(以非阻塞式),直到我们启动的后台Job结束:
import kotlinx.coroutines.*
fun main() = runBlocking {
//例子开始
val job = GlobalScope.launch { // 启动新协程,保留它的Job的引用
delay(1000L)
println("World!")
}
println("Hello,")
job.join() // 等待直到子协程结束
//例子结束
}
在这里获取完整代码
这个结果仍旧是相同的,但是主协程的代码,不再以任何方式,绑定到后台job的时长,好得多了!
结构化的并发
在实际使用协程时,有一些东西还是要改进的。当使用GlobalScope.launch时,我们创建了一个顶层协程。即使它是轻量级的,但是它运行时仍旧会消耗内存资源。如果我们忘记了保留新启动协程的引用,而这个协程还在运行。协程中代码 (hang)挂住 了(例如,我们错误地延迟太长时间),该怎么办?我们启动了太多协程而耗尽了内存,该怎么办?为所有启动的协程不得不手动地保留引用,然后join它们,这很容易出错。
有个更好的解决方案。我们可以在代码中使用结构化的并发。我们不是在GlobalScope中启动协程,就像我们经常处理线程一样(线程一直是全局的),而是在我们执行操作的特定作用域里面启动协程。
在这个例子中,我们有main函数,它用runBlocking协程生成器变成一个协程。每个协程生成器,包括runBlocking,把CoroutineScope的实例加入到它的代码块的作用域中。我们可以在这个作用域中启动协程,而不要显式地join它们,这是因为外层的作用域(这个例子中的runBlocking),一直等到这个作用域中的所有协程都结束时才结束。因此,我们可以简化这个例子:
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch { // runBlocking作用域中启动新协程
delay(1000L)
println("World!")
}
println("Hello,")
}
在这里获取完整代码
作用域生成器
除了由不同生成器提供的协程作用域,使用coroutineScope生成器声明我们自己的作用域也是可能的。它创建了新的协程作用域,而且直到所有启动的子协程结束时才结束。runBlocking和coroutineScope主要区别在于,后者不会阻塞当前协程,而是等待所有子协程结束。
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") // 这一行会在嵌入的launch之前打印
}
println("Coroutine scope is over") // 这一行直至所有嵌入的launch结束时才打印
}
在这里获取完整代码
提取函数的重构
让我们提取launch { … }里面的代码块到一个独立函数。当你对这个代码执行“提取函数”重构时,你会得到一个带有suspend修饰的新函数。这就是你第一个挂起函数(suspending function)。挂起函数可以在协程内使用,就像普通函数一样,但是它们的附加特性是,它们转而可以使用其他的挂起函数来挂起一个协程的执行,像这个例子中的delay。
import kotlinx.coroutines.*
fun main() = runBlocking {
launch { doWorld() }
println("Hello,")
}
// 这是你的第一个挂起函数
suspend fun doWorld() {
delay(1000L)
println("World!")
}
在这里获取完整代码
但是这个提取函数包含了一个协程生成器,它被当前作用域调用了,怎么办?这种情况下,提取函数的suspend修饰符是不够的。CoroutineScope上生成一个doWorld扩展函数是解决方案之一。但是它可能不总是可行的,因为它没有使得API更清楚。一个习惯解决方案是,一个显式的CoroutineScope,作为一个类的字段,这个类包含了目标函数,或者有一个隐式的CoroutineScope,当外部类实现了CoroutineScope。万不得已,也可以使用CoroutineScope(coroutineContext) ,但是这个方法在结构上是不安全的,因为你不再可以控制这个方法执行的作用域了。仅仅在私有API可以使用这个生成器。
协程 是 轻量级的
运行如下代码:
import kotlinx.coroutines.*
fun main() = runBlocking {
repeat(100_000) { // 启动大量协程
launch {
delay(1000L)
print(".")
}
}
}
在这里获取完整代码
它启动了十万个协程,一秒后,每个协程打印了一个点。现在,用线程试试。会发生什么呢?(最可能的是,我们的代码会出现某种内存溢出的错误)
全局协程就像守护线程
如下代码在GlobalScope上启动了一个长期运行的协程,每一秒打印"I’m sleeping"两次,然后在某个延迟之后从主函数返回:
import kotlinx.coroutines.*
fun main() = runBlocking {
//例子开始
GlobalScope.launch {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L) // 延迟后退出
//例子结束
}
在这里获取完整代码
你可以运行和看到它打印了三行然后终止:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
GlobalScope里启动的活跃协程没有让进程保活。它们就像守护线程。[注:GlobalScope不会绑定到任何job]