目录

异常处理

本节介绍异常处理和异常的取消。我们已经知道,取消的协程会在挂起点上引发 CancellationException,并且协程机制会忽略它。但是,如果在取消过程中引发异常或同一个协程的多个子协程发异常,会发生什么呢?

异常传播

协程构建器有两种形式:自动传播异常(launchactor)或向用户暴露异常(asyncproduce)。前者将异常视为未处理的异常,类似于Java的Thread.uncaughtExceptionHandler,而后者则依靠用户使用最终异常,例如通过 awaitreceive(produce和receive将在Channels部分中介绍)。

可以借助GlobalScope中创建协程的简单示例来演示:

  1. import kotlinx.coroutines.*
  2. fun main() = runBlocking {
  3. val job = GlobalScope.launch {
  4. println("Throwing exception from launch")
  5. throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
  6. }
  7. job.join()
  8. println("Joined failed job")
  9. val deferred = GlobalScope.async {
  10. println("Throwing exception from async")
  11. throw ArithmeticException() // Nothing is printed, relying on user to call await
  12. }
  13. try {
  14. deferred.await()
  15. println("Unreached")
  16. } catch (e: ArithmeticException) {
  17. println("Caught ArithmeticException")
  18. }
  19. }

这段代码的输出如下(附带调试信息):

  1. Throwing exception from launch
  2. Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException
  3. Joined failed job
  4. Throwing exception from async
  5. Caught ArithmeticException

协程异常处理器

但是,如果不想将所有异常打印到控制台怎么办? CoroutineExceptionHandler 上下文元素可以视为协程的通用 catch 块,在协程中可能发生自定义日志记录或异常处理。它类似于使用Thread.uncaughtExceptionHandler。

在JVM上,可以通过ServiceLoader注册CoroutineExceptionHandler来为所有协程重新定义全局异常处理程序。全局异常处理程序与Thread.defaultUncaughtExceptionHandler相似,在没有其他特定的处理程序注册时使用。在Android上,uncaughtExceptionPreHandler被安装为全局协程异常处理程序。

仅在用户未处理的异常上会调用CoroutineExceptionHandler,因此在async生成器等中注册它无效。

  1. val handler = CoroutineExceptionHandler { _, exception ->
  2. println("Caught $exception")
  3. }
  4. val job = GlobalScope.launch(handler) {
  5. throw AssertionError()
  6. }
  7. val deferred = GlobalScope.async(handler) {
  8. throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
  9. }
  10. joinAll(job, deferred)

此代码的输出是:

  1. Caught java.lang.AssertionError

取消和异常

取消与异常紧密相关。协程在内部使用CancellationException进行取消,所有处理程序都将忽略这些异常,因此它们仅应用作其他调试信息的源,可以通过catch块获取这些信息。使用 Job.cancel 取消协程时,协程终止,但不取消其父级。

  1. val job = launch {
  2. val child = launch {
  3. try {
  4. delay(Long.MAX_VALUE)
  5. } finally {
  6. println("Child is cancelled")
  7. }
  8. }
  9. yield()
  10. println("Cancelling child")
  11. child.cancel()
  12. child.join()
  13. yield()
  14. println("Parent is not cancelled")
  15. }
  16. job.join()

此代码的输出是:

  1. Cancelling child
  2. Child is cancelled
  3. Parent is not cancelled

如果协程遇到除CancellationException以外的其他异常,它将取消具有该异常的父对象。此行为不能被覆盖,并且用于为不依赖于CoroutineExceptionHandler实现的结构化并发提供稳定的协程层次结构。当父级的所有子级终止时,父级将处理原始异常。

这也是为什么在这些示例中,CoroutineExceptionHandler始终注册到在GlobalScope中创建的协程的原因。将异常处理程序注册到在主runBlocking范围内启动的协程中没有意义,因为尽管安装了该处理程序,但当其子级异常完成时,主协程将始终被取消。

  1. val handler = CoroutineExceptionHandler { _, exception ->
  2. println("Caught $exception")
  3. }
  4. val job = GlobalScope.launch(handler) {
  5. launch { // the first child
  6. try {
  7. delay(Long.MAX_VALUE)
  8. } finally {
  9. withContext(NonCancellable) {
  10. println("Children are cancelled, but exception is not handled until all children terminate")
  11. delay(100)
  12. println("The first child finished its non cancellable block")
  13. }
  14. }
  15. }
  16. launch { // the second child
  17. delay(10)
  18. println("Second child throws an exception")
  19. throw ArithmeticException()
  20. }
  21. }
  22. job.join()

此代码的输出是:

  1. Second child throws an exception
  2. Children are cancelled, but exception is not handled until all children terminate
  3. The first child finished its non cancellable block
  4. Caught java.lang.ArithmeticException

异常聚合

如果协程的多个子级抛出异常会怎样?一般规则是“第一个异常获胜”,因此第一个引发的异常向处理器暴露。但这可能导致丢失的异常,例如,协程在其 finally 块中抛出异常。因此,抑制了其他异常。

解决方案之一是分别报告每个异常,但是Deferred.await应该具有相同的机制来避免行为不一致,这将导致协程的实现细节(无论它是否将工作的一部分委派给了孩子或不)泄漏到其异常处理程序。

  1. import kotlinx.coroutines.*
  2. import java.io.*
  3. fun main() = runBlocking {
  4. val handler = CoroutineExceptionHandler { _, exception ->
  5. println("Caught $exception with suppressed ${exception.suppressed.contentToString()}")
  6. }
  7. val job = GlobalScope.launch(handler) {
  8. launch {
  9. try {
  10. delay(Long.MAX_VALUE)
  11. } finally {
  12. throw ArithmeticException()
  13. }
  14. }
  15. launch {
  16. delay(100)
  17. throw IOException()
  18. }
  19. delay(Long.MAX_VALUE)
  20. }
  21. job.join()
  22. }

注意:以上代码仅在支持抑制异常的JDK7 +上才能正常工作

此代码的输出是:

  1. Caught java.io.IOException with suppressed [java.lang.ArithmeticException]

请注意,该机制当前仅在Java版本1.7+上有效。 JS和Native的限制是暂时的,将来会修复。

取消异常是透明的,默认情况下是未包装的:

  1. val handler = CoroutineExceptionHandler { _, exception ->
  2. println("Caught original $exception")
  3. }
  4. val job = GlobalScope.launch(handler) {
  5. val inner = launch {
  6. launch {
  7. launch {
  8. throw IOException()
  9. }
  10. }
  11. }
  12. try {
  13. inner.join()
  14. } catch (e: CancellationException) {
  15. println("Rethrowing CancellationException with original cause")
  16. throw e
  17. }
  18. }
  19. job.join()

此代码的输出是:

  1. Rethrowing CancellationException with original cause
  2. Caught original java.io.IOException

监管

正如我们之前研究的那样,取消是在整个协程层次中传播的双向关系。但是,如果需要单向取消怎么办?

此类需求的一个很好的例子是在其范围内定义了工作的UI组件。如果UI的任何子任务失败,则不必总是取消(有效地杀死)整个UI组件,但是如果UI组件被破坏(并且其工作被取消),则必须使所有子任务失败,因为他们的结果不再需要。

另一个示例是一个服务器进程,该进程产生多个子作业,并且需要监督它们的执行,跟踪其失败并仅重新启动那些失败的子作业。

监管 job

为此,可以使用SupervisorJob。 它与常规Job相似,唯一的不同是取消仅向下传播。 举一个例子:

  1. import kotlinx.coroutines.*
  2. fun main() = runBlocking {
  3. val supervisor = SupervisorJob()
  4. with(CoroutineScope(coroutineContext + supervisor)) {
  5. // launch the first child -- its exception is ignored for this example (don't do this in practice!)
  6. val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) {
  7. println("First child is failing")
  8. throw AssertionError("First child is cancelled")
  9. }
  10. // launch the second child
  11. val secondChild = launch {
  12. firstChild.join()
  13. // Cancellation of the first child is not propagated to the second child
  14. println("First child is cancelled: ${firstChild.isCancelled}, but second one is still active")
  15. try {
  16. delay(Long.MAX_VALUE)
  17. } finally {
  18. // But cancellation of the supervisor is propagated
  19. println("Second child is cancelled because supervisor is cancelled")
  20. }
  21. }
  22. // wait until the first child fails & completes
  23. firstChild.join()
  24. println("Cancelling supervisor")
  25. supervisor.cancel()
  26. secondChild.join()
  27. }
  28. }

此代码的输出是:

  1. First child is failing
  2. First child is cancelled: true, but second one is still active
  3. Cancelling supervisor
  4. Second child is cancelled because supervisor is cancelled

监管作用域

对于有作用域的并发,出于相同的目的,可以使用 supervisorScope 代替 coroutineScope。它仅在一个方向上传播取消,并且仅在失败后才取消所有子级。它也像coroutineScope一样等待所有孩子完成任务。

  1. import kotlin.coroutines.*
  2. import kotlinx.coroutines.*
  3. fun main() = runBlocking {
  4. try {
  5. supervisorScope {
  6. val child = launch {
  7. try {
  8. println("Child is sleeping")
  9. delay(Long.MAX_VALUE)
  10. } finally {
  11. println("Child is cancelled")
  12. }
  13. }
  14. // Give our child a chance to execute and print using yield
  15. yield()
  16. println("Throwing exception from scope")
  17. throw AssertionError()
  18. }
  19. } catch(e: AssertionError) {
  20. println("Caught assertion error")
  21. }
  22. }

此代码的输出是:

  1. Child is sleeping
  2. Throwing exception from scope
  3. Child is cancelled
  4. Caught assertion error

监管协程中的异常

常规job和监管job之间的另一个关键区别是异常处理。每个子job都应通过异常处理机制自行处理其异常。这种差异来自于子job的失败不会传播给父job.

  1. import kotlin.coroutines.*
  2. import kotlinx.coroutines.*
  3. fun main() = runBlocking {
  4. val handler = CoroutineExceptionHandler { _, exception ->
  5. println("Caught $exception")
  6. }
  7. supervisorScope {
  8. val child = launch(handler) {
  9. println("Child throws an exception")
  10. throw AssertionError()
  11. }
  12. println("Scope is completing")
  13. }
  14. println("Scope is completed")
  15. }

此代码的输出是:

  1. Scope is completing
  2. Child throws an exception
  3. Caught java.lang.AssertionError
  4. Scope is completed