跳至主要內容

Supervising Coroutines

guodongAndroid大约 3 分钟

前言

到目前为止,我们已经讨论了异常如何在协程层次结构中传播。如果一个子线程抛出了异常,异常将传播至根协程。但是,当不想要这种行为时,会发生什么呢?例如,可能有一个具有自己作用域的 UI 组件。如果你在该作用域内创建一个子协程,并且该协程失败,而 UI 组件不得被取消。但是,如果该 UI 组件的作用域被取消了,那么所有的子任务也应该被取消。事实上,在协程工具包中有包含这种情况的工具——supervisorJobsupervisorScope

SupervisorJob

SupervisorJob 与常规的 Job 类似,唯一的区别是,取消只是向下传播。这意味着子线程抛出异常不会取消它们的父协程。让我们在示例中看看。打开 SupervisorJob.kt 文件,其中有一个空的 main 函数,用以下代码替换它:

fun main() = runBlocking {
  // 1
  val supervisor = SupervisorJob()
  with(CoroutineScope(coroutineContext + supervisor)) {
	// 2
    val firstChild = launch {
      println("First child throwing an exception")
      throw ArithmeticException()
    }
	// 3
    val secondChild = launch {
      println("First child is cancelled: ${firstChild.isCancelled}")
      try {
        delay(5000)
      } catch (e: CancellationException) {
        println("Second child cancelled because supervisor got cancelled.")
	  }
    }
	// 4
    firstChild.join()
    println("Second child is active: ${secondChild.isActive}")
    supervisor.cancel()
    secondChild.join()
  }
}

输出:

First child throwing an exception
First child is cancelled: true
Second child is active: true
Second child cancelled because supervisor got cancelled.
Exception in thread "main" java.lang.ArithmeticException ...

下面是对上述代码的解析:

  1. 你创建了一个 SupervisorJob 的实例,然后创建了一个新的 CoroutineScope,并将刚创建的 Job 作为其上下文的一部分。
  2. 你创建的第一个子协程抛出 ArithmeticException()
  3. 你创建的第二个子协程先打印第一个子协程的 取消 状态,然后延迟 5 秒钟并捕获 CancellationException,当 supervisor 取消时 。
  4. 等待第一个子线程执行完毕。然后你打印第二个子协程的状态来确保它仍在运行。然后你取消了 supervisor 并等待第二个子线程执行完毕。此时,secondChild 中的 catch 块将因CancellationException 而触发。

SupervisorScope

还记得本章之前的的 提示 吗(译者注:详见 CoroutineExceptionHandler),说 async 块有时会向上传播异常,而你无法捕获它?现在让我们检查一下那种情况,看看我们如何使用 SupervisorScope 来改变这种行为。在你的入门项目中打开 SupervisorScope.kt 并添加以下代码:

fun main() = runBlocking {
  val result = async {
    println("Throwing exception in async")
    throw IllegalStateException()
  }
  try {
    result.await()
  } catch (e: Exception) {
    println("Caught $e")
  }
}

输出:

Throwing exception in async
Caught java.lang.IllegalStateException
Exception in thread "main" java.lang.IllegalStateException

如你所见,即使你在 catch 块中捕获了异常,它仍然向上传播并重新抛出。为了解决这个问题,你应该使用 supervisorScope。它只是一个挂起函数,可以创建一个新的 CoroutineScope,并在该作用域中运行提供的挂起块。新的作用域是使用 SupervisorJob 创建的,这意味着子协程在失败时不会相互影响。以下是应用了修复程序的相同代码:

fun main() = runBlocking {
  supervisorScope {
    val result = async {
      println("Throwing exception in async")
      throw IllegalStateException()
    }
    try {
      result.await()
    } catch (e: Exception) {
      println("Caught $e")
    }
  }
}

输出:

Throwing exception in async
Caught java.lang.IllegalStateException

如你所见,现在异常不会向上传播并重新抛出了。这是因为 supervisorScope 让协程自己处理异常而不是传播至父协程。