目录

协程基础

这一部分包括基础的协程概念。

第一个协程程序

运行以下代码:

  1. import kotlinx.coroutines.*
  2. fun main() {
  3. GlobalScope.launch { // 在后台启动一个新的协程并继续
  4. delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
  5. println("World!") // 在延迟后打印输出
  6. }
  7. println("Hello,") // 协程已在等待时主线程还在继续
  8. Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
  9. }

可以在这里获取完整代码。

代码运行的结果:

  1. Hello,
  2. World!

本质上,协程是轻量级的线程。 它们在某些 CoroutineScope 上下文中与 launch 协程构建器 一起启动。 这里我们在 GlobalScope 中启动了一个新的协程,这意味着新协程的生命周期只受整个应用程序的生命周期限制。

可以将 GlobalScope.launch { …… } 替换为 thread { …… },并将 delay(……) 替换为 Thread.sleep(……) 达到同样目的。 试试看(不要忘记导入 kotlin.concurrent.thread)。

如果你首先将 GlobalScope.launch 替换为 thread,编译器会报以下错误:

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

这是因为 delay 是一个特殊的 挂起函数 ,它不会造成线程阻塞,但是会 挂起 协程,并且只能在协程中使用。

桥接阻塞与非阻塞的世界

第一个示例在同一段代码中混用了 非阻塞的 delay(……)阻塞的 Thread.sleep(……)。 这容易让我们记混哪个是阻塞的、哪个是非阻塞的。 让我们显式使用 runBlocking 协程构建器来阻塞:

  1. import kotlinx.coroutines.*
  2. fun main() {
  3. GlobalScope.launch { // 在后台启动一个新的协程并继续
  4. delay(1000L)
  5. println("World!")
  6. }
  7. println("Hello,") // 主线程中的代码会立即执行
  8. runBlocking { // 但是这个表达式阻塞了主线程
  9. delay(2000L) // ……我们延迟 2 秒来保证 JVM 的存活
  10. }
  11. }

可以在这里获取完整代码。

结果是相似的,但是这些代码只使用了非阻塞的函数 delay。 调用了 runBlocking 的主线程会一直 阻塞 直到 runBlocking 内部的协程执行完毕。

这个示例可以使用更合乎惯用法的方式重写,使用 runBlocking 来包装 main 函数的执行:

  1. import kotlinx.coroutines.*
  2. fun main() = runBlocking<Unit> { // 开始执行主协程
  3. GlobalScope.launch { // 在后台启动一个新的协程并继续
  4. delay(1000L)
  5. println("World!")
  6. }
  7. println("Hello,") // 主协程在这里会立即执行
  8. delay(2000L) // 延迟 2 秒来保证 JVM 存活
  9. }

可以在这里获取完整代码。

这里的 runBlocking<Unit> { …… } 作为用来启动顶层主协程的适配器。 我们显式指定了其返回类型 Unit,因为在 Kotlin 中 main 函数必须返回 Unit 类型。

这也是为挂起函数编写单元测试的一种方式:

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

等待一个作业

延迟一段时间来等待另一个协程运行并不是一个好的选择。让我们显式(以非阻塞方式)等待所启动的后台 Job 执行结束:

  1. import kotlinx.coroutines.*
  2. fun main() = runBlocking {
  3. //sampleStart
  4. val job = GlobalScope.launch { // 启动一个新协程并保持对这个作业的引用
  5. delay(1000L)
  6. println("World!")
  7. }
  8. println("Hello,")
  9. job.join() // 等待直到子协程执行结束
  10. //sampleEnd
  11. }

可以在这里获取完整代码。

现在,结果仍然相同,但是主协程与后台作业的持续时间没有任何关系了。好多了。

结构化的并发

协程的实际使用还有一些需要改进的地方。 当我们使用 GlobalScope.launch 时,我们会创建一个顶层协程。虽然它很轻量,但它运行时仍会消耗一些内存资源。如果我们忘记保持对新启动的协程的引用,它还会继续运行。如果协程中的代码挂起了会怎么样(例如,我们错误地延迟了太长时间),如果我们启动了太多的协程并导致内存不足会怎么样? 必须手动保持对所有已启动协程的引用并 join 之很容易出错。

有一个更好的解决办法。我们可以在代码中使用结构化并发。 我们可以在执行操作所在的指定作用域内启动协程, 而不是像通常使用线程(线程总是全局的)那样在 GlobalScope 中启动。

在我们的示例中,我们使用 runBlocking 协程构建器将 main 函数转换为协程。 包括 runBlocking 在内的每个协程构建器都将 CoroutineScope 的实例添加到其代码块所在的作用域中。 我们可以在这个作用域中启动协程而无需显式 join 之,因为外部协程(示例中的 runBlocking)直到在其作用域中启动的所有协程都执行完毕后才会结束。因此,可以将我们的示例简化为:

  1. import kotlinx.coroutines.*
  2. fun main() = runBlocking { // this: CoroutineScope
  3. launch { // 在 runBlocking 作用域中启动一个新协程
  4. delay(1000L)
  5. println("World!")
  6. }
  7. println("Hello,")
  8. }

可以在这里获取完整代码。

作用域构建器

除了由不同的构建器提供协程作用域之外,还可以使用 coroutineScope 构建器声明自己的作用域。它会创建一个协程作用域并且在所有已启动子协程执行完毕之前不会结束。

runBlockingcoroutineScope 可能看起来很类似,因为它们都会等待其协程体以及所有子协程结束。 主要区别在于,runBlocking 方法会阻塞当前线程来等待, 而 coroutineScope 只是挂起,会释放底层线程用于其他用途。 由于存在这点差异,runBlocking 是常规函数,而 coroutineScope 是挂起函数。

可以通过以下示例来演示:

  1. import kotlinx.coroutines.*
  2. fun main() = runBlocking { // this: CoroutineScope
  3. launch {
  4. delay(200L)
  5. println("Task from runBlocking")
  6. }
  7. coroutineScope { // 创建一个协程作用域
  8. launch {
  9. delay(500L)
  10. println("Task from nested launch")
  11. }
  12. delay(100L)
  13. println("Task from coroutine scope") // 这一行会在内嵌 launch 之前输出
  14. }
  15. println("Coroutine scope is over") // 这一行在内嵌 launch 执行完毕后才输出
  16. }

可以在这里获取完整代码。

请注意,(当等待内嵌 launch 时)紧挨“Task from coroutine scope”消息之后, 就会执行并输出“Task from runBlocking”——尽管 coroutineScope 尚未结束。

提取函数重构

我们来将 launch { …… } 内部的代码块提取到独立的函数中。当你对这段代码执行“提取函数”重构时,你会得到一个带有 suspend 修饰符的新函数。 这是你的第一个挂起函数。在协程内部可以像普通函数一样使用挂起函数, 不过其额外特性是,同样可以使用其他挂起函数(如本例中的 delay)来挂起协程的执行。

  1. import kotlinx.coroutines.*
  2. fun main() = runBlocking {
  3. launch { doWorld() }
  4. println("Hello,")
  5. }
  6. // 这是你的第一个挂起函数
  7. suspend fun doWorld() {
  8. delay(1000L)
  9. println("World!")
  10. }

可以在这里获取完整代码。

但是如果提取出的函数包含一个在当前作用域中调用的协程构建器的话,该怎么办? 在这种情况下,所提取函数上只有 suspend 修饰符是不够的。为 CoroutineScope 写一个 doWorld 扩展方法是其中一种解决方案,但这可能并非总是适用,因为它并没有使 API 更加清晰。 惯用的解决方案是要么显式将 CoroutineScope 作为包含该函数的类的一个字段, 要么当外部类实现了 CoroutineScope 时隐式取得。 作为最后的手段,可以使用 CoroutineScope(coroutineContext),不过这种方法结构上不安全, 因为你不能再控制该方法执行的作用域。只有私有 API 才能使用这个构建器。

协程很轻量

运行以下代码:

  1. import kotlinx.coroutines.*
  2. fun main() = runBlocking {
  3. repeat(100_000) { // 启动大量的协程
  4. launch {
  5. delay(5000L)
  6. print(".")
  7. }
  8. }
  9. }

可以在这里获取完整代码。

它启动了 10 万个协程,并且在 5 秒钟后,每个协程都输出一个点。

现在,尝试使用线程来实现。会发生什么?(很可能你的代码会产生某种内存不足的错误)

全局协程像守护线程

以下代码在 GlobalScope 中启动了一个长期运行的协程,该协程每秒输出“I’m sleeping”两次,之后在主函数中延迟一段时间后返回。

  1. import kotlinx.coroutines.*
  2. fun main() = runBlocking {
  3. //sampleStart
  4. GlobalScope.launch {
  5. repeat(1000) { i ->
  6. println("I'm sleeping $i ...")
  7. delay(500L)
  8. }
  9. }
  10. delay(1300L) // 在延迟后退出
  11. //sampleEnd
  12. }

可以在这里获取完整代码。

你可以运行这个程序并看到它输出了以下三行后终止:

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

GlobalScope 中启动的活动协程并不会使进程保活。它们就像守护线程。