本文是对 Kotlin Coroutines by Tutorials By Filip Babić and Nishant Srivastava 书本内容的转述和笔记,书本内容是受原版权保护的内容。
Chapter 7: 异常处理
Exception Propagation 异常传递
你可以用多种方式构建一个协程。你使用的协程构建器的种类决定了异常将如何传播,以及如何处理它们。
当使用
launch和actor协程构建器时,异常会自动传播并被视为未处理的,类似于 Java 的Thread.uncaughExceptionHandler。当使用
async和produce协程构建器时,异常会暴露给用户,以便在协程执行结束时通过await或receive最终消费。
了解异常是如何传播的,有助于找出正确的策略来处理它们。
Handling Exceptions 处理异常
异常处理在协程中是非常直接的。如果代码抛出了一个异常,上下文会自动传播它,你不需要做任何事情。协程使异步代码看起来是同步的,类似于处理同步代码的预期方式──也就是说,try-catch
也适用于协程。
下面是一个简单的例子,它在 GlobalScope
中创建了新的协程,并从不同的协程构建器中抛出了异常:
fun main() = runBlocking {
val asyncJob = GlobalScope.launch {
println("1. Exception created via launch coroutine")
// Will be printed to the console by
// Thread.defaultUncaughtExceptionHandler
throw IndexOutOfBoundsException()
}
asyncJob.join()
println("2. Joined failed job")
val deferred = GlobalScope.async {
println("3. Exception created via async coroutine")
// Nothing is printed, relying on user to call await
throw ArithmeticException()
}
try {
deferred.await()
println("4. Unreachable, this statement is never executed")
} catch (e: Exception) {
println("5. Caught ${e.javaClass.simpleName}")
}
}Output:
1. Exception created via launch coroutine
Exception in thread "DefaultDispatcher-worker-1" java.lang.IndexOutOfBoundsException
- - -
2. Joined failed job
3. Exception created via async coroutine
5. Caught ArithmeticException在前面的代码中,你使用 GlobalScope.launch
协程构建器启动了一个协程,并在其主体中抛出一个
IndexOutOfBoundsException。这是一个正常异常传播的例子,它由默认的
Thread.uncaughExceptionHandler
实现处理。这是一个负责管理程序中抛出的未处理异常的对象。它只是将异常传播给调用者的线程处理程序(如果有的话),或者将其信息打印在标准输出上。在本例中,你已经进入了主函数,所以错误信息是输出的一部分。
如你所知,GlobalScope.launch 创建了一个 Job
实例,你对它调用了 join 函数。第一个
Job,因为出现了异常,所以输出了
2. Joined failed job。在第二个协程中,你使用
GlobalScope.async 协程构建器,它在其主体中抛出一个
ArithmeticException。在这种情况下,这个异常在被创建的时候不会被
Thread.uncaughExceptionHandler 处理,但可以被
GlobalScope.async 返回的 Deferred 对象上调用的
await 函数抛出。在这种情况下,可能的异常也被推迟了。
CoroutineExceptionHandler
类似于使用 Java 的
Thread.defaultUncaughtExceptionHandler,它为未捕获的线程异常返回一个处理程序,协程提供了一个可选的通用
catch 块来处理未捕获的异常,称为
CoroutineExceptionHandler。
注意:在 Android 上,
uncaughtExceptionPreHandler是全局的协程异常处理程序。
通常情况下,未捕获的异常只能由使用启动协程构造器创建的协程导致。使用
async
创建的协程总是捕捉所有的异常,并在产生的
Deferred 对象中表示它们。
当使用 launch 构造器时,异常将被存储在一个
Job 对象中。要检索它,你可以使用
invokeOnCompletion 辅助函数。
fun main() {
runBlocking {
val job = GlobalScope.launch {
println("1. Exception created via launch coroutine")
// Will NOT be handled by
// Thread.defaultUncaughtExceptionHandler
// since it is being handled later by `invokeOnCompletion`
throw IndexOutOfBoundsException()
}
// Handle the exception thrown from `launch` coroutine builder
job.invokeOnCompletion { exception ->
println("2. Caught $exception")
}
// This suspends coroutine until this job is complete.
job.join()
}
}Output:
1. Exception created via launch coroutine
Exception in thread "main" java.lang.IndexOutOfBoundsException
....
2. Caught java.lang.IndexOutOfBoundsException默认情况下,当你没有设置处理程序时,系统会按照以下顺序处理未捕获的异常:
- 如果异常是
CancellationException,那么系统会忽略它,因为那是取消运行中的协程的机制。 - 否则,如果上下文中有一个
Job,那么Job.cancel就会被调用。 - 否则,通过
ServiceLoader和当前线程的Thread.uncaughtExceptionHandler找到的所有CoroutineExceptionHandler实例,全部都将会被调用。
注意:1.
Job.cancel优先与其他exceptionHandler2.CoroutineExceptionHandler只在那些不期望被用户处理的异常中被调用,所以在异步协程构建器中注册它,以及类似的东西没有任何作用。
下面是一个简单的例子来演示 CoroutineExceptionHandler
的用法:
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)
}
}Output:
Caught java.lang.AssertionError: My Custom Assertion Error!这里是对代码块的解释: - 实现一个全局异常处理程序;即
CoroutineExceptionHandler。这是你定义当一个未处理的协程抛出异常时如何处理的地方。
- 使用协程构造器创建一个简单的协程,抛出一个自定义消息
AssertionError - 使用 async
协程构造器创建一个简单的协程,抛出一个
ArithmeticException。 - joinAll
用于挂起当前的循环程序,直到所有给定的工作都完成。
CoroutineExceptionHandler
在你想有一个全局的异常处理程序在协程之间共享时很有用,但如果你想以不同的方式处理特定协程的异常,你需要提供具体的实现。让我们来看看如何实现。
Try-Catch to the
Rescue
当涉及到为一个特定的协程处理异常时,你可以使用 try-catch
块来捕捉异常,并像你在用 Kotlin 进行正常的同步编程时那样处理它们。
但是有一个问题。如果你不小心,用 async
构建器创建的协程通常会「吞噬」异常。如果一个异常在 async
块中被抛出,该异常实际上不会被立即抛出。相反,它将在你对返回的
Deferred 对象调用 await
时被抛出。这种行为,如果不加以考虑,可能会导致没有异常被跟踪的情况,但将异常处理推迟到稍后的时间也可能是一个理想的行为,这取决于你实际的用例。
这里有一个例子来证明这一点。
fun main() {
runBlocking {
// Set this to ’true’ to call await on the deferred variable
val callAwaitOnDeferred = true
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")
}
}
}
}Output:
callAwaitOnDeferred被设置为false的情况下的输出──也就是说,没有调用await的情况下:
1. Throwing exception from asynccallAwaitOnDeferred被设置为true的情况下的输出–也就是说,有调用await的情况下:
1. Throwing exception from async
2. Caught ArithmeticExceptionHandling Multiple Child Coroutine Exceptions 处理多个子协程异常
只拥有一个协程是一个理想的用例。在实践中,你可能会有多个协程,并在其下运行其他子协程。如果这些子协程抛出异常会怎样?这就是所有这些可能变得棘手的地方。在这种情况下,一般的规则是「第一个异常赢」。如果你设置了一个
CoroutineExceptionHandler,它将只管理第一个异常,而抑制其他所有的异常。
下面是一个例子来证明这一点:
fun main() = runBlocking {
// Global Exception Handler
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception with suppressed " +
// Get the suppressed exception
"${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()
}Output:
JobCancellationException in Child Job 1
Caught java.lang.IllegalStateException with suppressed [java.lang.ArithmeticException]在前面的例子中:
- 你定义了一个
CoroutineExceptionHandler来打印第一个被捕获的异常的名字,以及它从被抑制的属性中获得的被抑制的异常。 - 在这之后,你使用
launch协程构建器启动一个父协程,将异常处理程序作为参数。父协程包含几个子协程,你再次使用launch函数来启动这些子协程。第一个循环程序包含一个try-catch-finally块。 - 在
try块中,你用一个Long.MAX_VALUE参数值调用delay函数,以便等待很长一段时间。 - 在
catch中,你打印一个关于捕获异常的信息。 - 最后,你抛出一个
ArithmeticException。 - 在第二个协程中,你只延迟了几毫秒,然后立即抛出一个
IllegalStateException。 - 然后你完成了父协程,在另一个很长的时间段内调用
delay函数。 - 主函数的最后一条行代码允许程序等待父
Job的完成。
当你运行这段代码时,父协程开始,其子协程也开始。第一个子协程在等待,第二个子程序抛出一个
IllegalStateException,这是处理程序将管理的第一个异常,你可以在输出中看到。正因为如此,系统因此强制取消了第一个协程的
delay,这就是
JobCancellationException 消息的原因。这也使得父
Job 失败,因此,处理程序将被调用并显示其输出。
简而言之,在这种嵌套
Job关系中,一个出事全部cancel,exception.suppressed是个数组,存着所有由cancel导致的异常
需要注意的是,CoroutineExceptionHandler
是父协程的一部分(在父协程的上下文中),所以它可以处理在它作用域下的所有异常。
Callback Wrapping 包装回调
处理异步代码的执行通常需要实现某种回调机制。
例如,对于一个异步网络调用,你可能希望有 onSuccess 和
onFailure 回调,这样你就可以适当地处理这两种情况。
这样的代码往往会变得相当复杂,难以阅读。幸运的是,协程提供了一种包装回调的方法,通过协程库中的
suspendCoroutine
挂起函数,将异步代码处理的复杂性从调用者那里隐藏起来。它捕获了当前的
Continuation 实例并挂起了当前运行的协程。
Continuation
提供了两个函数,你可以用它们恢复协程的执行。调用 resume
函数可以恢复协程的执行并返回一个值,而 resumeWithException
则在最后一个挂起点之后重新抛出异常。
resume 是通过在未来在可挂起函数中安排调用
Continuation 方法来完成的。
看一个简单的长期运行的 Job
的例子,它有一个处理结果的回调。你将把回调包在一个协程中,并大大简化了工作:
fun main() {
runBlocking {
try {
val data = getDataAsync()
println("Data received: $data")
} catch (e: Exception) {
println("Caught ${e.javaClass.simpleName}")
}
}
}
// Callback Wrapping using Coroutine
suspend fun getDataAsync(): String {
return suspendCoroutine { cont ->
getData(object : AsyncCallback {
override fun onSuccess(result: String) {
cont.resumeWith(Result.success(result))
}
override fun onError(e: Exception) {
cont.resumeWith(Result.failure(e))
}
})
}
}
// Method to simulate a long running task
fun getData(asyncCallback: AsyncCallback) {
// Flag used to trigger an exception
val triggerError = false
try {
// Delaying the thread for 3 seconds
Thread.sleep(3000)
if (triggerError) {
throw IOException()
} else {
// Send success
asyncCallback.onSuccess("[Beep.Boop.Beep]")
}
} catch (e: Exception) {
// send error
asyncCallback.onError(e)
}
}
// Callback
interface AsyncCallback {
fun onSuccess(result: String)
fun onError(e: Exception)
}Output:
当 triggerError 字段在 getData()
方法中被设置为 false 时。
Data received: [Beep.Boop.Beep]当 triggerError 字段在 getData()
方法中被设置为 true 时。
Caught IOException