Kotlin协程的异常处理
捕获异常
Kotlin协程中执行的代码如果可能发生异常,最简单直接的办法也是可以通过 try-catch 语句来捕获异常
GlobalScope.launch {
try {
println(1 / 0)
} catch (e: Exception) {
//can catch exception
}
}
但try-catch只能捕获该协程代码块中发生的异常,对于子协程中发生的异常则无能为力:
GlobalScope.launch {
try {
val child = launch {
println(1 / 0)
}
child.join()
} catch (e: Exception) {
//can not catch exception
}
}
Kotlin协程中的异常传递机制
当协程中的代码执行发生未捕获的异常时,会取消当前发生异常的协程及其子协程(如若当前协程有子协程)的执行,然后将异常传递给父协程并取消父协程。
父协程也是按照上述处理方式,取消自己以及子协程的执行,然后继续将向上传递异常并取消自己的父协程。
上述机制可以在 JobSupport 中找到对应的源码:
private fun finalizeFinishingState(state: Finishing, proposedUpdate: Any?): Any? {
//省略....
if (finalException != null) {
val handled = cancelParent(finalException) || handleJobException(finalException)
if (handled) (finalState as CompletedExceptionally).makeHandled()
}
}
//省略....
}
子协程发生异常时会先调用 cancelParent 方法,将异常传递给父协程并尝试取消父协程。如果父协程不处理,则会由自己调用 handleJobException 方法来处理。
private open class StandaloneCoroutine(
parentContext: CoroutineContext,
active: Boolean
) : AbstractCoroutine<Unit>(parentContext, initParentJob = true, active = active) {
override fun handleJobException(exception: Throwable): Boolean {
handleCoroutineException(context, exception)
return true
}
}
public fun handleCoroutineException(context: CoroutineContext, exception: Throwable) {
// Invoke an exception handler from the context if present
try {
context[CoroutineExceptionHandler]?.let {
it.handleException(context, exception)
return
}
} catch (t: Throwable) {
handleCoroutineExceptionImpl(context, handlerException(exception, t))
return
}
// If a handler is not present in the context or an exception was thrown, fallback to the global handler
handleCoroutineExceptionImpl(context, exception)
}
handleJobException 方法中会找到当前Job中设置的 CoroutineExceptionHandler 来处理异常,找不到则会重新抛出异常最终导致Crash。
CoroutineExceptionHandler是CoroutineContext.Element的一个子类,只包含一个 handleException 方法:
public interface CoroutineExceptionHandler : CoroutineContext.Element {
public fun handleException(context: CoroutineContext, exception: Throwable)
}
根据上述异常传递机制,异常最终会传递到根协程来处理,而根协程的parent job为null,因此异常由根协程调用 handleJobException 方法来处理异常,即由根协程中设置的 CoroutineExceptionHandler 来处理。
如下代码所示,我们在根协程中设置了 CoroutineExceptionHandler 并打印出子协程中发生的异常
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
GlobalScope.launch(handler) {
launch { // the first child
delay(10)
println(1 / 0)
}
launch { // the second child
println("Second child start")
delay(100)
println("Second child end")
}
}
上述代码输出如下:
Second child start
CoroutineExceptionHandler got java.lang.ArithmeticException: / by zero
可以看出第一个子协程中发生的异常在根协程中设置的 CoroutineExceptionHandler 中打印出来了,并且由于第一个子协程发生了异常,第二个子协程也被取消(最后的print语句没有执行到)。
特殊的是,对于使用 async 方法发起的协程如果作为根协程,其异常是在调用 await 方法时才会抛出。
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val deferred = GlobalScope.async(handler) {
println("Throwing exception from async")
throw ArithmeticException()
}
deferred.await()
此外,对于async发起的协程设置 CoroutineExceptionHandler 是不生效的,原因是async发起的协程其对应的Job实现并不是StandaloneCoroutine,而是DeferredCoroutine, DeferredCoroutine中的 handleJobException 方法仍然是 JobSupport 中的默认实现固定返回false,因此 async 发起的协程中发生的异常只能交给父协程来处理,而上述示例中async 发起的协程是根协程没有父协程,因此异常一定会被抛出而导致crash。
而如果async发起的协程不作为根协程,则其抛出的异常仍然能正常被根协程所处理,如下代码所示:
val handler = CoroutineExceptionHandler { _, exception ->
println("Root got $exception")
}
GlobalScope.launch(handler) {
val job = launch {
delay(3000)
}
val deferred = async {
println("Throwing exception from async")
throw ArithmeticException()
}
job.join()
}
上述代码输出如下结果,可见async抛出的异常被根协程中设置的CoroutineExceptionHandler所捕获。
Throwing exception from async
Root got java.lang.ArithmeticException
SupervisorJob 与 supervisorScope
SupervisorJob 是一个函数,返回一个 SupervisorJobImpl 实例:
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)
private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
override fun childCancelled(cause: Throwable): Boolean = false
}
SupervisorJobImpl 继承自 JobImpl 类,覆写了 childCancelled 方法的实现固定返回false,其子协程调用 cancelParent 方法取消它时不会生效,因而其子协程发生的异常最终调用 handleJobException 方法处理,即Supervisor Job隔绝了子协程发生的异常继续向上传递的路径。
val supervisor = SupervisorJob()
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
GlobalScope.launch(handler) {
// launch the first child
val firstChild = launch(supervisor) {
println("The first child is failing")
throw AssertionError("The first child is cancelled")
}
// launch the second child
val secondChild = launch {
delay(2000)
println("The second child completed")
}
joinAll(firstChild, secondChild)
}
上述代码输出如下结果:
The first child is failing
CoroutineExceptionHandler got java.lang.AssertionError: The first child is cancelled
The second child completed
由于 firstChild 的parent job是 SupervisorJob,其发生异常后调用 cancelParent 方法不会取消父协程,也就不会导致其兄弟协程被取消。
需要注意的是上述示例中的异常最终并不是交给根协程处理的,而是交给发生异常的子协程自己处理的,因为子协程实际上继承了根协程的 CorourtineExceptionHandler 。
supervisorScope 是一个函数,使用 supervisorScope 函数发起的协程其父协程是 SupervisorCoroutine,SupervisorCoroutine 的 Job 即是Supervisor Job。前述示例代码也可以使用supervisorScope来改造:
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
supervisorScope {
// launch the first child
val firstChild = launch(handler) {
println("The first child is failing")
throw AssertionError("The first child is cancelled")
}
// launch the second child
val secondChild = launch {
delay(2000)
println("The second child completed")
}
joinAll(firstChild, secondChild)
}