Kotlin协程的异常处理

发布时间 2023-10-10 20:01:01作者: jqc

捕获异常

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协程中的异常传递机制

当协程中的代码执行发生未捕获的异常时,会取消当前发生异常的协程及其子协程(如若当前协程有子协程)的执行,然后将异常传递给父协程并取消父协程。
父协程也是按照上述处理方式,取消自己以及子协程的执行,然后继续将向上传递异常并取消自己的父协程。

image

上述机制可以在 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。

CoroutineExceptionHandlerCoroutineContext.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隔绝了子协程发生的异常继续向上传递的路径。

image

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)
}