Kotlin 协程中的异常传递是一个复杂但灵活的机制,允许开发者在不同的协程上下文中安全处理异常,并控制协程的生命周期。以下是 Kotlin 协程的异常传递和处理的详细说明,包括异常传播方式、不同的上下文和作用域的影响、以及捕获异常的最佳实践。
1. 协程的异常传播基础
在 Kotlin 协程中,异常通常会传播到协程的 父级 或 启动上下文,并会根据上下文的 CoroutineExceptionHandler 来处理。未捕获的异常会停止当前协程,并向上级传播。
2. 异常处理原则
协程的异常处理受以下几个因素影响:
- 上下文作用域:协程的上下文决定了异常的传播路径,例如
SupervisorJob可以隔离子协程的异常,防止影响其他子协程。 - 启动模式:启动模式(如
launch或async)决定了异常是立即传播还是延迟到调用await时传播。 - CoroutineExceptionHandler:用于捕获未处理的异常,是协程中的全局异常处理器。
3. launch 与 async 的异常处理差异
launch
launch会立即执行协程,并且在协程体中抛出的异常会立即传播。- 如果
launch协程没有try-catch,则异常会向上抛到其所在的作用域。 - 如果作用域没有
CoroutineExceptionHandler处理异常,应用程序将崩溃。
kotlin
复制代码
GlobalScope.launch {
throw Exception("Launch scope exception") // 异常会立即传播
}
async
async主要用于返回结果的协程,不会立即传播异常。- 异常会在调用
await()方法时抛出,这使得async的异常处理更适合延迟捕获的场景。
kotlin
复制代码
val deferred = GlobalScope.async {
throw Exception("Async scope exception") // 不会立即传播
}
try {
deferred.await() // 在此处捕获异常
} catch (e: Exception) {
println("Caught async exception: ${e.message}")
}
4. 使用 CoroutineExceptionHandler 捕获未处理的异常
CoroutineExceptionHandler 是一个全局异常处理器,适合捕获未处理的协程异常,尤其适合应用于顶级协程。它可以添加到协程上下文中,自动捕获未处理的异常。
kotlin
复制代码
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught exception: ${exception.message}")
}
GlobalScope.launch(handler) {
throw Exception("Unhandled exception")
}
5. supervisorScope 与 SupervisorJob 的异常隔离
在父子协程关系中,异常会默认影响整个作用域中的其他协程。然而,supervisorScope 和 SupervisorJob 可以帮助实现异常隔离,使一个子协程的失败不会导致其他协程取消。
supervisorScope
- 使用
supervisorScope可以独立处理各个子协程的异常,使某个子协程异常不会影响其他协程。
kotlin
复制代码
runBlocking {
supervisorScope {
launch {
throw Exception("Child coroutine exception") // 该异常不会影响 supervisorScope 中的其他协程
}
launch {
println("Other coroutine running independently")
}
}
}
SupervisorJob
SupervisorJob是可以附加到协程作用域上的一种Job,它能实现类似supervisorScope的效果。
kotlin
复制代码
val scope = CoroutineScope(SupervisorJob())
scope.launch {
launch {
throw Exception("Child coroutine exception") // 不会导致其他协程取消
}
launch {
println("Other coroutine unaffected")
}
}
6. 异常传播的规则总结
- 子协程向父协程传播:如果一个子协程抛出异常,默认会取消整个作用域。
- SupervisorJob 的例外:使用
SupervisorJob可以在作用域中隔离异常。 - CoroutineExceptionHandler 的优先级:如果协程具有
CoroutineExceptionHandler,则异常会首先由它处理,不会继续向上传递。
7. 捕获取消异常:CancellationException
CancellationException 是协程中用于标识协程取消的异常,通常不会向上层传播。需要注意的是,协程在处理 CancellationException 时,建议在需要忽略该异常时进行手动处理。
kotlin
复制代码
val job = GlobalScope.launch {
try {
delay(Long.MAX_VALUE) // 假设这是一个需要长时间执行的任务
} catch (e: CancellationException) {
println("Coroutine was cancelled")
}
}
job.cancel(CancellationException("Cancelled manually"))
8. 综合示例
以下代码展示了如何在多个协程作用域中处理异常、传播异常以及如何使用 CoroutineExceptionHandler 捕获顶级异常:
kotlin
复制代码
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught exception in CoroutineExceptionHandler: ${exception.message}")
}
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch {
launch {
throw Exception("First child exception") // 不会影响其他协程
}
launch {
delay(100)
println("Second child unaffected by first child's exception")
}
}.join() // 等待子协程完成
}
总结
Kotlin 协程的异常处理和传播提供了细粒度的控制:
launch会立即传播异常,async则在await()时传播异常。CoroutineExceptionHandler是用于捕获未处理异常的全局处理器。supervisorScope和SupervisorJob用于隔离异常,使一个协程的失败不会影响其他协程。CancellationException表示协程的取消,一般不向上层传播。