1. 你的第一个协程
fun main() {
GlobalScope.launch { // launch a 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
}
Hello,
World!
GlobalScope.launch
是一个位于某个 CoroutineScope
中的 coroutine builder,我们通过该方法在 GlobalScope
中启动一个新的协程。GlobalScope
是在生命周期方面和 Java Thread
类似的一个执行环境,也就说 GlobalScope
的生命周期将不受调用函数的作用域、实例生命周期的限制,如果它的执行时间足够长,那么 GlobalScope
将在应用的整个生命周期中占用资源。
2. 连接阻塞和非阻塞两个世界
在我们上面的例子中,有两个「延时」的方法: delay
和 Thread.sleep
。但是 delay
是一个「非阻塞」方法, Thread.sleep
是一个「阻塞」方法,混合使用只会让人很难分清哪一个在「阻塞」线程,而哪一个没有。我们可以将其统一成「非阻塞」方法来达到「延时」的目的:
fun main() {
GlobalScope.launch { // launch a 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
}
}
上面的代码能达到同样的效果,但是使用了
runBlocking
来显式地表示这是一个「阻塞」的方法。runBlocking
会一直「阻塞」直到它其中的子协程全部都依次执行完(非子协程部分会并行于子协程执行)。
我们可以再改写下上面的代码,使其开起来更加协程化:
fun main() = runBlocking<Unit> { // start main coroutine
GlobalScope.launch { // launch a 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
}
上面的 runBlocking<Unit>
中可以显示地指明泛型。该泛型表示协程的返回类型,由于 delay
的返回类型就是 Unit
,因此在上面的例子中, runBlocking
的泛型是没有必要的(可以通过最后一行的 delay
推断出来)。
3. Waiting for a Job
我们上面的代码是通过时间来控制两次打印(两次任务)的执行先后顺序,事实上我们应该让两个任务无缝连接地先后执行。
fun main() = runBlocking {
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
}
每个启动的协程块都会返回一个 Job
对象使调用者可以控制该协程。
3. 结构化的并发
虽然每个协程块都会返回一个 Job
对象,但是如果我们显式地通过持有 Job
去管理协程,当有多个协程时将会非常难以管理。
我们有更好的方法:使用结构化并发。前面的代码我们很难管理的一个原因是,我们在 GlobalScope
中启动的协程,它的生命周期默认是跟应用程序的生命周期一样的,但是事实上,我们可以在我们自己的作用域中去启动协程。
每一个 coroutine builder,包括 runBlocking
,都会向它的代码块提供一个 CoroutineScope
的实例。在作用域中启动的子协程不用显式地调用 join
来同步与其父协程的顺序关系,因为父协程会等待所有由它启动的子协程都结束后才会结束。所以,我们可以将例子代码这样写:
fun main() = runBlocking { // this: CoroutineScope
launch { // launch a new coroutine in the scope of runBlocking
delay(1000L)
println("World!")
}
println("Hello,")
}
4. Scope builder
除了 runBlocking
和 GlobalScope.launch
这样的协程构造器中它们自带的 coroutine scope 外,你还可以通过调用 coroutineScope
构造器来申明你自己的 coroutine scope。 coroutineScope
会创建一个新的 coroutine scope ,同其他所有的 coroutine scope 一样,只有它的子协程全部都结束后,它才会结束。
runBlocking
和 coroutineScope
看起来差不多,但是它们之间有个最主要的区别,就是 runBlocking
会阻塞它的调用者,而 coroutineScope
只会将它的调用者线程挂起。正是这个区别,因此 runBlocking
是一个普通函数,而 coroutineScope
则是一个挂起函数。
4.1 runBlocking, coroutineScope, launch 之间的区别
首先我们定义一个函数用于计算函数调用的时刻。
private fun timeDelta(startTimestamp: Long) = (System.currentTimeMillis() - startTimestamp)
我们先看下 runBlocking
和 launch
之间的特性:
// 代码清单 scope-builder-1
fun main() = runBlocking<Unit> { // this: CoroutineScope
val startTimestamp = System.currentTimeMillis()
launch {
val delay = 200L
delay(delay)
println("${timeDelta(startTimestamp)}: runBlocking 中的任务 1 ($delay)")
}
launch {
val delay = 50L
delay(delay)
println("${timeDelta(startTimestamp)}: runBlocking 中的任务 2 ($delay)")
}
println("${timeDelta(startTimestamp)}: runBlocking 执行完毕") // This line is not printed until the nested launch completes
}
5: runBlocking 执行完毕
70: runBlocking 中的任务 2 (50)
214: runBlocking 中的任务 1 (200)
我们可以发现,任务1 的开始时间 214 并没有在 任务2 的结束时间 70 上累加(如果累加的话, 任务1 的开始时间应为 284),因此 launch
之间其实是并发地执行。我们再看看 runBlocking
和 coroutineScope
之间的关系:
// 代码清单 scope-builder-2
fun main() = runBlocking<Unit> { // this: CoroutineScope
val startTimestamp = System.currentTimeMillis()
coroutineScope {
val delay = 150L
delay(delay)
println("${timeDelta(startTimestamp)}: 执行第一个 coroutineScope 里的协程($delay)")
}
println("${timeDelta(startTimestamp)}: 第一个 coroutineScope 结束")
coroutineScope { // Creates a coroutine scope
launch {
val delay = 50L
delay(delay)
println("${timeDelta(startTimestamp)}: 执行第二个 coroutineScope 中的任务($delay)")
}
val delay = 10L
delay(delay)
println("${timeDelta(startTimestamp)}: 执行第二个 coroutineScope 里的协程($delay)") // This line will be printed before the nested launch
}
println("${timeDelta(startTimestamp)}: runBlocking 执行完毕") // This line is not printed until the nested launch completes
}
167: 执行第一个 coroutineScope 里的协程(150)
168: 第一个 coroutineScope 结束
182: 执行第二个 coroutineScope 里的协程(10)
222: 执行第二个 coroutineScope 中的任务(50)
223: runBlocking 执行完毕
可以看到 coroutineScope
之间是依次顺序执行的。
Q: 为什么 代码清单 scope-builder-1 是并发地执行,而 代码清单 scope-builder-2 是阻塞地执行呢?
A: 我们先看下 launch
的源码:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
// 使用 EmptyCoroutineContext 构造新的 context
val newContext = newCoroutineContext(context)
// 使用新构造的 context 来构造一个 Coroutine 对象
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
// 执行这个新的 Coroutine
coroutine.start(start, coroutine, block)
return coroutine
}
从上面我们可以看到,虽然 launch
构造了新的 CoroutineContext
和新的 Coroutine ,但是没有构造新的 Scope。
再看下 coroutineScope
的源码:
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
// 通过 ScopeCoroutine 创建了新的 Scope
val coroutine = ScopeCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
我们可以看到 coroutineScope
通过 ScopeCoroutine
构造了新的 Scope。
因此, 如果协程运行在新的 Scope 中,那么这些新的 Scope 之间会阻塞地执行。如果只是通过 launch
等形式运行协程,那么这些协程之间会并行执行 。
A: runBlocking
和 coroutineScope
的区别呢?
Q: runBlocking
的「阻塞」性质并不是指的「阻塞」子 Scope 使其顺序执行,而是指的它不是一个「挂起」函数。 coroutineScope
和 runBlocking
一样都会等待其子 Scope 顺序执行完,区别只是 runBlocking
是一个普通函数,会阻塞调用者线程,而 coroutineScope
是一个挂起函数,不会阻塞调用者线程:
fun main() = runBlocking<Unit> { // this: CoroutineScope
val startTimestamp = System.currentTimeMillis()
coroutineScope {
val delay = 150L
delay(delay)
println("${timeDelta(startTimestamp)}: 执行第一个 coroutineScope 里的协程($delay)")
coroutineScope {
val delay1 = 200L
delay(delay1)
println("${timeDelta(startTimestamp)}: 执行第一个 coroutineScope 里的子 scope_1($delay1)")
}
coroutineScope {
val delay2 = 100L
delay(delay2)
println("${timeDelta(startTimestamp)}: 执行第二个 coroutineScope 里的子 scope_2($delay2)")
}
}
println("${timeDelta(startTimestamp)}: 第一个 coroutineScope 结束")
println("${timeDelta(startTimestamp)}: runBlocking 执行完毕") // This line is not printed until the nested launch completes
}、
168: 执行第一个 coroutineScope 里的协程(150)
369: 执行第一个 coroutineScope 里的子 scope_1(200)
474: 执行第二个 coroutineScope 里的子 scope_2(100)
474: 第一个 coroutineScope 结束
474: runBlocking 执行完毕
可以看到 scope_1 和 scope_2 之间是顺序执行的。