本文是对 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 {
("1. Exception created via launch coroutine")
println
// Will be printed to the console by
// Thread.defaultUncaughtExceptionHandler
throw IndexOutOfBoundsException()
}
.join()
asyncJob("2. Joined failed job")
println
val deferred = GlobalScope.async {
("3. Exception created via async coroutine")
println
// Nothing is printed, relying on user to call await
throw ArithmeticException()
}
try {
.await()
deferred("4. Unreachable, this statement is never executed")
println} catch (e: Exception) {
("5. Caught ${e.javaClass.simpleName}")
println}
}
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 {
("1. Exception created via launch coroutine")
println
// 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
.invokeOnCompletion { exception ->
job("2. Caught $exception")
println}
// This suspends coroutine until this job is complete.
.join()
job}
}
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
优先与其他exceptionHandler
2.CoroutineExceptionHandler
只在那些不期望被用户处理的异常中被调用,所以在异步协程构建器中注册它,以及类似的东西没有任何作用。
下面是一个简单的例子来演示 CoroutineExceptionHandler
的用法:
fun main() {
{
runBlocking // 1
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
("Caught $exception")
println}
// 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.
(job, deferred)
joinAll}
}
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()
("Throwing exception from async")
printlnthrow ArithmeticException("Something Crashed")
// Nothing is printed, relying on a call to await()
}
if (callAwaitOnDeferred) {
try {
.await()
deferred} catch (e: ArithmeticException) {
("Caught ArithmeticException")
println}
}
}
}
Output:
callAwaitOnDeferred
被设置为false
的情况下的输出──也就是说,没有调用await
的情况下:
1. Throwing exception from async
callAwaitOnDeferred
被设置为true
的情况下的输出–也就是说,有调用await
的情况下:
1. Throwing exception from async
2. Caught ArithmeticException
Handling Multiple Child Coroutine Exceptions 处理多个子协程异常
只拥有一个协程是一个理想的用例。在实践中,你可能会有多个协程,并在其下运行其他子协程。如果这些子协程抛出异常会怎样?这就是所有这些可能变得棘手的地方。在这种情况下,一般的规则是「第一个异常赢」。如果你设置了一个
CoroutineExceptionHandler
,它将只管理第一个异常,而抑制其他所有的异常。
下面是一个例子来证明这一点:
fun main() = runBlocking {
// Global Exception Handler
val handler = CoroutineExceptionHandler { _, exception ->
("Caught $exception with suppressed " +
println
// Get the suppressed exception
"${exception.suppressed?.contentToString()}")
}
// Parent Job
val parentJob = GlobalScope.launch(handler) {
// Child Job 1
{
launch try {
(Long.MAX_VALUE)
delay} catch (e: Exception) {
("${e.javaClass.simpleName} in Child Job 1")
println} finally {
throw ArithmeticException()
}
}
// Child Job 2
{
launch (100)
delaythrow IllegalStateException()
}
// Delaying the parentJob
(Long.MAX_VALUE)
delay}
// Wait until parentJob completes
.join()
parentJob}
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()
("Data received: $data")
println} catch (e: Exception) {
("Caught ${e.javaClass.simpleName}")
println}
}
}
// Callback Wrapping using Coroutine
fun getDataAsync(): String {
suspend return suspendCoroutine { cont ->
(object : AsyncCallback {
getDataoverride fun onSuccess(result: String) {
.resumeWith(Result.success(result))
cont}
override fun onError(e: Exception) {
.resumeWith(Result.failure(e))
cont}
})
}
}
// 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
.sleep(3000)
Thread
if (triggerError) {
throw IOException()
} else {
// Send success
.onSuccess("[Beep.Boop.Beep]")
asyncCallback}
} catch (e: Exception) {
// send error
.onError(e)
asyncCallback}
}
// 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