[译] Kotlin 协程异常处理

原文:https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/topics/exception-handling.md


本节介绍异常处理和在异常时取消。我们已经知道取消的协程会在挂起点抛出 CancellationException 并且它会被协程的机制忽略。这里我们看看如果在取消过程中抛出异常或者同一个协程的多个子协程抛出异常会发生什么。

异常传播

协程构建器有两种形式:自动传播异常(launch 和 actor),或者将它们暴露于用户(async 和 produce)。当这些构建器用于创建一个协程,它不是另一个协程的协程时,前者构建器将异常视为未捕获的异常,类似于 Java 的 Thread.uncaughtExceptionHandler,而后者则依赖于用户来消费最终异常,例如通过 await 或 receive(produce 和 receive 将在后面的 Channels 部分中介绍)。

可以通过一个使用 GlobalScope 创建根协程的简单示例来演示:

GlobalScope 是一个微妙的 API,以非平凡的方式可能会适得其反。为整个应用程序创建根协程是 GlobalScope 的罕见合法用途之一,因此您必须明确选择 @OptIn(DelicateCoroutinesApi::class) 使用 GlobalScope。

import kotlinx.coroutines.*

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val job = GlobalScope.launch { // root coroutine with launch
        println("Throwing exception from launch")
        throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
    }
    job.join()
    println("Joined failed job")
    val deferred = GlobalScope.async { // root coroutine with async
        println("Throwing exception from async")
        throw ArithmeticException() // Nothing is printed, relying on user to call await
    }
    try {
        deferred.await()
        println("Unreached")
    } catch (e: ArithmeticException) {
        println("Caught ArithmeticException")
    }
}

这段代码的输出是(带debug):

Throwing exception from launch
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException
Joined failed job
Throwing exception from async
Caught ArithmeticException

协程异常处理器 CoroutineExceptionHandler

可以自定义将未捕获的异常打印到控制台的默认行为。 协程上的 CoroutineExceptionHandler 上下文元素可用作此根协程及其所有自定义异常处理可能发生的子级的通用 catch 块,其中可能发生自定义异常处理。它类似于 Thread.uncaughtExceptionHandler。您无法在 CoroutineExceptionHandler 中从异常恢复。当异常处理器被调用时,协程已经用相应的异常来完成了。通常,异常处理器用于记录异常、显示某种错误消息、终止和/或重新启动应用程序。

在 JVM 上,可以通过 ServiceLoader 注册 CoroutineExceptionHandler 来为所有协程重新定义全局异常处理器。 全局异常处理器类似于 Thread.defaultUncaughtExceptionHandler ,它在当没有注册更多的特定处理器时使用。 在 Android 上,uncaughtExceptionPreHandler 被安装为全局协程异常处理器。

CoroutineExceptionHandler 仅在未捕获的异常(未以任何其他方式处理的异常)上调用。 特别是,所有协程(在另一个 Job 的上下文中创建的协程)将其异常的处理委托给它们的父协程,父协程也委托给父协程,依此类推直到根,因此永远不会使用安装在其上下文中的 CoroutineExceptionHandler。除此之外,async 构建器始终捕获所有异常并将它们表示在生成的 Deferred 对象中,因此它的 CoroutineExceptionHandler 也不起作用。

在 supervision scope 内运行的协程不会将异常传播到其父级,因此被排除在此规则之外。 本文档的 Supervision 部分提供了更多详细信息。

import kotlinx.coroutines.*

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
//sampleStart
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("CoroutineExceptionHandler got $exception") 
    }
    val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope
        throw AssertionError()
    }
    val deferred = GlobalScope.async(handler) { // also root, but async instead of launch
        throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
    }
    joinAll(job, deferred)
//sampleEnd    
}

这段代码的输出是:

CoroutineExceptionHandler got java.lang.AssertionError

取消和异常

取消与异常密切相关。协程内部使用 CancellationException 来取消,这些异常会被所有异常处理器忽略,因此它们应该仅用作附加调试信息的来源,可以通过 catch 块获取。当使用 Job.cancel 取消协程时,它会终止,但不会取消其父项。

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    val job = launch {
        val child = launch {
            try {
                delay(Long.MAX_VALUE)
            } finally {
                println("Child is cancelled")
            }
        }
        yield()
        println("Cancelling child")
        child.cancel()
        child.join()
        yield()
        println("Parent is not cancelled")
    }
    job.join()
//sampleEnd    
}

这段代码的输出是:

Cancelling child
Child is cancelled
Parent is not cancelled

如果协程遇到除 CancellationException 之外的异常,它会使用该异常取消其父协程。此行为不能被覆盖,用于为结构化并发提供稳定的协程层次 结构。 CoroutineExceptionHandler 实现不用于子协程。

在这些示例中,CoroutineExceptionHandler 始终安装到在 GlobalScope 中创建的协程。将异常处理器安装到在主 runBlocking scope 内启动的协程是没有意义的,因为尽管安装了异常处理器,但当其子协程以异常完成时,主协程将始终被取消。

原始异常仅在其所有子级终止时才由父级处理,如下例所示。

import kotlinx.coroutines.*

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
//sampleStart
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("CoroutineExceptionHandler got $exception") 
    }
    val job = GlobalScope.launch(handler) {
        launch { // the first child
            try {
                delay(Long.MAX_VALUE)
            } finally {
                withContext(NonCancellable) {
                    println("Children are cancelled, but exception is not handled until all children terminate")
                    delay(100)
                    println("The first child finished its non cancellable block")
                }
            }
        }
        launch { // the second child
            delay(10)
            println("Second child throws an exception")
            throw ArithmeticException()
        }
    }
    job.join()
//sampleEnd 
}

这段代码的输出是:

Second child throws an exception
Children are cancelled, but exception is not handled until all children terminate
The first child finished its non cancellable block
CoroutineExceptionHandler got java.lang.ArithmeticException

异常聚合

当协程的多个子协程因异常而失败时,一般规则是“第一个异常获胜”,因此第一个异常得到处理。在第一个异常之后发生的所有其他异常都作为抑制异常附加到第一个异常。

import kotlinx.coroutines.*
import java.io.*

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}")
    }
    val job = GlobalScope.launch(handler) {
        launch {
            try {
                delay(Long.MAX_VALUE) // it gets cancelled when another sibling fails with IOException
            } finally {
                throw ArithmeticException() // the second exception
            }
        }
        launch {
            delay(100)
            throw IOException() // the first exception
        }
        delay(Long.MAX_VALUE)
    }
    job.join()  
}

注意:以上代码仅在支持 suppressed 异常的 JDK7+ 上才能正常工作

这段代码的输出是:

CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]

请注意,此机制目前仅适用于 Java 1.7+ 版本。JS 和 Native 的限制是暂时的,将来会取消。

取消异常是透明的,默认情况下是展开的:

import kotlinx.coroutines.*
import java.io.*

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
//sampleStart
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    val job = GlobalScope.launch(handler) {
        val inner = launch { // all this stack of coroutines will get cancelled
            launch {
                launch {
                    throw IOException() // the original exception
                }
            }
        }
        try {
            inner.join()
        } catch (e: CancellationException) {
            println("Rethrowing CancellationException with original cause")
            throw e // cancellation exception is rethrown, yet the original IOException gets to the handler  **经测试,无论是否 rethrow,两处同样都会打印捕获异常日志**}
    }
    job.join()
//sampleEnd    
}

这段代码的输出是:

Rethrowing CancellationException with original cause
CoroutineExceptionHandler got java.io.IOException

Supervision

正如我们之前研究过的,取消是一种双向关系,在整个协程层次结构中传播。我们来看看需要单向取消的情况。

这种需求的一个很好的例子是在其 scope 内定义了作业的 UI 组件。如果 UI 的任何子任务失败,并不总是需要取消(有效地杀死)整个 UI 组件,但是如果 UI 组件被销毁(并且其作业被取消),那么有必要将所有子作业失败因为不再需要他们的结果。

另一个例子是一个服务器进程,它产生多个子作业,需要监督它们的执行,跟踪它们的失败并且只重新启动失败的那些。

Supervision job

SupervisorJob 可以用于这些目的。它类似于常规 Job,唯一的例外是取消仅向下传播。这可以使用以下示例轻松演示:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val supervisor = SupervisorJob()
    with(CoroutineScope(coroutineContext + supervisor)) {
        // launch the first child -- its exception is ignored for this example (don't do this in practice!)
        val firstChild = launch(CoroutineExceptionHandler { _, _ ->  }) {
            println("The first child is failing")
            throw AssertionError("The first child is cancelled")
        }
        // launch the second child
        val secondChild = launch {
            firstChild.join()
            // Cancellation of the first child is not propagated to the second child
            println("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active")
            try {
                delay(Long.MAX_VALUE)
            } finally {
                // But cancellation of the supervisor is propagated
                println("The second child is cancelled because the supervisor was cancelled")
            }
        }
        // wait until the first child fails & completes
        firstChild.join()
        println("Cancelling the supervisor")
        supervisor.cancel()
        secondChild.join()
    }
}

这段代码的输出是:

The first child is failing
The first child is cancelled: true, but the second one is still active
Cancelling the supervisor
The second child is cancelled because the supervisor was cancelled

Supervision scope

我们可以使用 supervisorScope 代替 coroutineScope 进行范围并发。它仅在一个方向上传播取消,并且仅当它自己失败时才取消其所有子项。它还像 coroutineScope 一样在完成之前等待所有子协程。

import kotlin.coroutines.*
import kotlinx.coroutines.*

fun main() = runBlocking {
    try {
        supervisorScope {
            val child = launch {
                try {
                    println("The child is sleeping")
                    delay(Long.MAX_VALUE)
                } finally {
                    println("The child is cancelled")
                }
            }
            // Give our child a chance to execute and print using yield 
            yield()
            println("Throwing an exception from the scope")
            throw AssertionError()
        }
    } catch(e: AssertionError) {
        println("Caught an assertion error")
    }
}

这段代码的输出是:

The child is sleeping
Throwing an exception from the scope
The child is cancelled
Caught an assertion error

Exceptions in supervised coroutines

常规作业和 supervisor 作业之间的另一个重要区别是异常处理。每个孩子都应该通过异常处理机制自己处理它的异常。这种差异来自这样一个事实,即孩子的失败不会传播给父母。这意味着直接在 supervisorScope 内启动的协程确实使用安装在其 scope 内的 CoroutineExceptionHandler,其方式与根协程相同(有关详细信息,请参阅 CoroutineExceptionHandler 部分)。

import kotlin.coroutines.*
import kotlinx.coroutines.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("CoroutineExceptionHandler got $exception") 
    }
    supervisorScope {
        val child = launch(handler) {
            println("The child throws an exception")
            throw AssertionError()
        }
        println("The scope is completing")
    }
    println("The scope is completed")
}

这段代码的输出是:

The scope is completing
The child throws an exception
CoroutineExceptionHandler got java.lang.AssertionError
The scope is completed
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值