CoroutineExceptionHandler
CoroutineExceptionHandler
在最后一个示例中,你可以看到 IndexOutOfBoundsException
被打印到控制台。你可以通过创建自定义 CoroutineExceptionHandler
来自定义该行为,该处理器作为根协程及其子协程的通用 catch
块。它类似于Thread.uncaughtExceptionHandler
,这意味着你无法通过使用它从异常中恢复。
需要注意的是,CoroutineExceptionHandler
仅捕获未以任何其他方式处理的异常,即:未捕获的 异常。
通常,未捕获的异常只能由使用 launch
协程构建器创建的协程产生。使用 async
创建的协程捕获其所有异常,并在生成的 Deferred
对象中表示它们。
提示
注意: 在某些情况下,async
会传播异常,你将无法在 catch
块中捕获它。
稍后我们再讨论它。
在你的 ExceptionHandling 入门项目中打开 GlobalExceptionHandler.kt 文件并将下面代码替换其中空的 main
函数:
@OptIn(DelicateCoroutinesApi::class)
fun main() {
runBlocking {
// 1
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
// 2
val job = GlobalScope.launch(exceptionHandler) {
throw AssertionError("My Custom Assertion Error!")
}
// 3
val deferred = GlobalScope.async(exceptionHandler) {
// Nothing will be printed,
// relying on user to call deferred.await()
throw ArithmeticException()
}
// 4
// This suspends current coroutine until all given jobs are complete.
joinAll(job, deferred)
}
}
输出:
Caught java.lang.AssertionError: My Custom Assertion Error!
以下是对代码块的解析:
- 实现一个全局异常处理器,比如:
CoroutineExceptionHandler
。这就是你定义如何处理未捕获异常的地方。 - 使用
launch
协程构建器创建了一个简单的协程,然后在其中抛出一个自定义信息的AssertionError
。 - 使用
async
协程构建器创建了一个简单的协程,然后在其中抛出一个ArithmeticException
。 joinAll
用于挂起当前协程,直到给定的任务全部完成。
当你想在协程之间共享全局异常处理器时,CoroutineExceptionHandler
非常有用,但如果你想以不同的方式处理特定协程的异常,则需要提供特定的实现。让我们看看你如何做到这一点。
提示
注意: CoroutineExceptionHandler
仅在用户期望不会处理的异常上调用,因此在 async
协程构建器中注入它看起来没有效果。
Try-Catch 救场
当涉及到处理特定协程的异常时,你可以使用 try/catch
块来捕获异常,并像在使用 Kotlin 进行正常同步编程时一样处理它们。
不过有一个陷阱。如果不小心,使用 async
协程构建器创建的协程通常会吞下异常。如果一个异常在 async
块执行期间抛出,则这个异常不会立即抛出。相反,它会在你调用返回的 Deferred
对象的 await()
函数时抛出。如果不考虑此行为,可能会导致无法跟踪异常的情况。另一方面,根据实际情况,将异常处理推迟到稍后的时间点也可能是理想的行为。
下面是一个例子来演示。在你的项目中打开 TryCatch.kt 文件并将以下代码替换到其空的 main
函数中:
@OptIn(DelicateCoroutinesApi::class)
fun main() {
runBlocking {
// Set this to 'true' to call await on the deferred variable
val callAwaitOnDeferred = false
val deferred = GlobalScope.async {
// This statement will be printed with or without a call to await()
println("Throwing exception from async")
throw ArithmeticException("Something Crashed")
// Nothing is printed, relying on a call to await()
}
if (callAwaitOnDeferred) {
try {
deferred.await()
} catch (e: ArithmeticException) {
println("Caught ArithmeticException")
}
}
}
}
callAwaitOnDeferred
设置为 false
情况下的输出——即没有调用 await
:
Throwing exception from async
callAwaitOnDeferred
设置为 true
情况下的输出——即调用 await
:
Throwing exception from async
Caught ArithmeticException
处理多个子协程异常
只有一个协程是一个理想的情况。在实践中,你可能有多个协程,其他子协程在它们下面运行。如果这些子协程抛出异常,会发生什么?一切都将可能变得棘手。在这种情况下,一般是 第一个异常胜出。如果你注入了一个 CoroutineExceptionHandler
,它将只处理第一个异常,而抑制所有其他的异常。
为了证明这点,在本章的入门项目中打开 ExceptionHandlingForChild.kt 文件并将以下代码替换到其空的 main
函数中:
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
// Global Exception Handler
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception with suppressed ${exception.suppressed?.contentToString()}")
}
// Parent Job
val parentJob = GlobalScope.launch(handler) {
// Child Job 1
launch {
try {
delay(Long.MAX_VALUE)
} catch (e: Exception) {
println("${e.javaClass.simpleName} in Child Job 1")
} finally {
throw ArithmeticException()
}
}
// Child Job 2
launch {
delay(100)
throw IllegalStateException()
}
// Delaying the parentJob
delay(Long.MAX_VALUE)
}
// Wait until parentJob completes
parentJob.join()
}
输出:
JobCancellationException in Child Job 1
Caught java.lang.IllegalStateException with suppressed [java.lang.ArithmeticException]
在上面的示例中:
- 你定义了一个
CoroutineExceptionHandler
来打印捕获的第一个异常的名称,以及从它的suppressed
属性中获取的被抑制的异常。 - 然后,你将这个异常处理器作为
launch
协程构建器的参数启动了一个父协程。这个父协程包含几个子协程,你再次使用launch
函数启动这些子协程。其中第一个子协程包含一个try-catch-finally
块。 - 在
try
块中,你调用非常大参数值的delay
函数,以便等待很长时间。 - 在
catch
块中,你打印了捕获异常的信息。 - 在
finally
块中,你抛出了一个ArithmeticException
。 - 在第二个子协程中,你延迟了几毫秒然后抛出了一个
IllegalStateException
。 - 然后完成父协程,在另一段长时间内调用
delay
函数。 main
函数的最后是允许程序等待parentJob
的完成。
当你运行此代码时,父协程启动,其子协程也会启动。第一个子协程等待时,第二个子协程抛出一个 IllegalStateException
,这是异常处理器管理的第一个异常,正如你在输出中看到的那样。因此,系统强制取消第一个子协程中的 delay
,这就是 JobCancellationException 信息的原因。同时这也导致父协程的失败,因此,将调用异常处理器并显示其输出。
需要注意的是,CoroutineExceptionHandler
是父协程的一部分,因此它管理与之相关的异常。