[TOC]
本节讨论协程关于异常的处理和取消异常。我们已经知道,取消协程会使得在挂起点抛出 CancellationException,而协程机制会忽略这个异常。但是,如果在取消期间抛出异常,或者协程的多个子协程抛出异常,此时会发生什么情况呢?
一、异常的传播(Exception propagation)
协程构建器有两种类型:自动传播异常(launch 和 actor)和向用户公开异常(async 和 product)。前者将异常视为未捕获异常,类似于 Java 的 Thread.uncaughtExceptionHandler,而后者则需要由开发者自己来处理最终的异常,例如通过 await 或 receive(product 和 receive 在 Channels 章节介绍)
可以通过在 GlobalScope 创建协程的简单示例来演示:
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = GlobalScope.launch {
println("Throwing exception from launch")
throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
}
job.join()
println("Joined failed job")
val deferred = GlobalScope.async {
println("Throwing exception from async")
throw ArithmeticException() // Nothing is printed, relying on user to call await
}
try {
deferred.await()
println("Unreached")
} catch (e: ArithmeticException) {
println("Caught ArithmeticException")
}
}
运行结果:
Throwing exception from launch
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException
Joined failed job
Throwing exception from async
Caught ArithmeticException
二、协程异常处理器(CoroutineExceptionHandler)
如果不想将所有的异常都打印到控制台上,CoroutineExceptionHandler 上下文元素可以作为协程全局通用的 catch 块,在这里进行自定义日志记录或异常处理。它类似于对线程使用 Thread.uncaughtExceptionHandler
在 JVM 上,可以通过 ServiceLoader 注册 CoroutineExceptionHandler 来重新定义所有协程的全局异常处理器。全局异常处理程序类似于 Thread.defaultUncaughtExceptionHandler ,后者在没有注册其它特定处理程序时使用。在 Android 上,uncaughtExceptionPreHandler 作为全局协程异常处理程序存在
CoroutineExceptionHandler 只在预计不会由用户处理的异常上调用,因此在 async 这类协程构造器中注册它没有任何效果
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val job = GlobalScope.launch(handler) {
throw AssertionError()
}
val deferred = GlobalScope.async(handler) {
throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
}
joinAll(job, deferred)
//sampleEnd
}
运行结果:
Caught java.lang.AssertionError
三、取消和异常(Cancellation and exceptions)
取消和异常是紧密联系的。协程在内部使用 CancellationException 来进行取消,所有处理程序都会忽略这类异常,因此它们仅用作调试信息的额外来源,这些信息可以用 catch 块捕获。当使用 Job.cancel 取消协程时,协程将停止运行,但不会取消其父协程
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val job = launch {
val child = launch {
try {
delay(Long.MAX_VALUE)
} finally {
println("Child is cancelled")
}
}
yield()
println("Cancelling child")
child.cancel()
child.join()
yield()
println("Parent is not cancelled")
}
job.join()
//sampleEnd
}
运行结果:
Cancelling child
Child is cancelled
Parent is not cancelled
如果协程遇到 CancellationException 以外的异常,它将用该异常取消其父级。无法重写此行为,它用于为不依赖于 CoroutineExceptionHandler 实现的结构化并发,为之提供稳定的协程层次结构。当父级的所有子级终止时,父级将处理原始异常
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val job = GlobalScope.launch(handler) {
launch { // the first child
try {
delay(Long.MAX_VALUE)
} finally {
withContext(NonCancellable) {
println("Children are cancelled, but exception is not handled until all children terminate")
delay(100)
println("The first child finished its non cancellable block")
}
}
}
launch { // the second child
delay(10)
println("Second child throws an exception")
throw ArithmeticException()
}
}
job.join()
//sampleEnd
}
运行结果:
Second child throws an exception
Children are cancelled, but exception is not handled until all children terminate
The first child finished its non cancellable block
Caught java.lang.ArithmeticException
四、异常聚合(Exceptions aggregation)
如果一个协程的多个子协程抛出异常会发生什么情况呢?一般的规则是“第一个异常会获胜”,因此第一个抛出的异常将传递给异常处理器进行处理,但这也有可能会导致异常丢失。例如,如果在某个协程在抛出异常后,第二个协程在其 finally 块中抛出异常,此时第二个协程的异常将不会传递给 CoroutineExceptionHandler
import kotlinx.coroutines.*
import java.io.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception with suppressed ${exception.suppressed.contentToString()}")
}
val job = GlobalScope.launch(handler) {
launch {
try {
delay(Long.MAX_VALUE)
} finally {
throw ArithmeticException()
}
}
launch {
delay(100)
throw IOException()
}
delay(Long.MAX_VALUE)
}
job.join()
}
运行结果:
Caught java.io.IOException with suppressed [java.lang.ArithmeticException]
导致协程停止的异常在默认情况下是会被透传,不会被包装的
import kotlinx.coroutines.*
import java.io.*
fun main() = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught original $exception")
}
val job = GlobalScope.launch(handler) {
val inner = launch {
launch {
launch {
throw IOException()
}
}
}
try {
inner.join()
} catch (e: CancellationException) {
println("Rethrowing CancellationException with original cause")
throw e
}
}
job.join()
//sampleEnd
}
运行结果:
Rethrowing CancellationException with original cause
Caught original java.io.IOException
五、监督(Supervision)
正如我们之前所研究的,取消是一种双向关系,会在整个协程层次结构中传播。但如果需要单向取消呢?
此类需求的一个很好的例子在某个范围内定义了 Job 的 UI 组件。如果 UI 组件的任意一个子任务失败了,此时并不一定需要取消(实际上是终止)整个 UI 组件。但是如果 UI 组件的生命周期结束了(并且取消了它的 Job),那么就必须取消所有子 Job, 因为它们的结果不再是必需的
另一个例子是一个服务器进程,它生成几个子 Job 并且需要监测它们的执行,跟踪它们的失败时机,并且仅重新启动那么失败的子 Job
5.1、监督作业(Supervision job)
出于这些目的,可以使用 SupervisorJob。它类似于常规的 Job,唯一的例外是取消操作只向下传播。用一个例子很容易演示:
import kotlinx.coroutines.*
fun main() = runBlocking {
val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)) {
// launch the first child -- its exception is ignored for this example (don't do this in practice!)
val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) {
println("First child is failing")
throw AssertionError("First child is cancelled")
}
// launch the second child
val secondChild = launch {
firstChild.join()
// Cancellation of the first child is not propagated to the second child
println("First child is cancelled: ${firstChild.isCancelled}, but second one is still active")
try {
delay(Long.MAX_VALUE)
} finally {
// But cancellation of the supervisor is propagated
println("Second child is cancelled because supervisor is cancelled")
}
}
// wait until the first child fails & completes
firstChild.join()
println("Cancelling supervisor")
supervisor.cancel()
secondChild.join()
}
}
运行结果:
First child is failing
First child is cancelled: true, but second one is still active
Cancelling supervisor
Second child is cancelled because supervisor is cancelled
5.2、监督作用域(Supervision scope)
对于作用域并发,可以使用 supervisorScope 代替 coroutineScope 来实现相同的目的。它只在一个方向上传播取消操作,并且仅在自身失败时才取消所有子级。它也像 coroutineScope 一样在结束运行之前等待所有的子元素结束运行
import kotlin.coroutines.*
import kotlinx.coroutines.*
fun main() = runBlocking {
try {
supervisorScope {
val child = launch {
try {
println("Child is sleeping")
delay(Long.MAX_VALUE)
} finally {
println("Child is cancelled")
}
}
// Give our child a chance to execute and print using yield
yield()
println("Throwing exception from scope")
throw AssertionError()
}
} catch(e: AssertionError) {
println("Caught assertion error")
}
}
输出结果:
Child is sleeping
Throwing exception from scope
Child is cancelled
Caught assertion error
以下例子展示了 supervisorScope 中取消操作的单向传播性,子协程的异常不会导致其它子协程取消
fun main() = runBlocking {
supervisorScope {
val child1 = launch {
try {
for (time in 1..Long.MAX_VALUE) {
println("Child 1 is printing: $time")
delay(1000)
}
} finally {
println("Child 1 is cancelled")
}
}
val child2 = launch {
println("Child 2 is sleeping")
delay(3000)
println("Child 2 throws an exception")
throw AssertionError()
}
}
}
运行结果:
Child 1 is printing: 1
Child 2 is sleeping
Child 1 is printing: 2
Child 1 is printing: 3
Child 1 is printing: 4
Child 2 throws an exception
Exception in thread "main" java.lang.AssertionError
Child 1 is printing: 5
Child 1 is printing: 6
······
5.3、监督协程中的异常(Exceptions in supervised coroutines)
常规 job 和 supervisor job 的另一个重要区别在于异常处理。每个子级都应该通过异常处理机制自己处理其异常。这种差异来自于这样一个事实:supervisorScope 中子元素的失败不会传导给父级
import kotlin.coroutines.*
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
supervisorScope {
val child = launch(handler) {
println("Child throws an exception")
throw AssertionError()
}
println("Scope is completing")
}
println("Scope is completed")
}
运行结果:
Scope is completing
Child throws an exception
Caught java.lang.AssertionError
Scope is completed