详细介绍kotlin协程的异常传递

421 阅读4分钟

Kotlin 协程中的异常传递是一个复杂但灵活的机制,允许开发者在不同的协程上下文中安全处理异常,并控制协程的生命周期。以下是 Kotlin 协程的异常传递和处理的详细说明,包括异常传播方式、不同的上下文和作用域的影响、以及捕获异常的最佳实践。

1. 协程的异常传播基础

在 Kotlin 协程中,异常通常会传播到协程的 父级启动上下文,并会根据上下文的 CoroutineExceptionHandler 来处理。未捕获的异常会停止当前协程,并向上级传播。

2. 异常处理原则

协程的异常处理受以下几个因素影响:

  • 上下文作用域:协程的上下文决定了异常的传播路径,例如 SupervisorJob 可以隔离子协程的异常,防止影响其他子协程。
  • 启动模式:启动模式(如 launchasync)决定了异常是立即传播还是延迟到调用 await 时传播。
  • CoroutineExceptionHandler:用于捕获未处理的异常,是协程中的全局异常处理器。

3. launchasync 的异常处理差异

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. supervisorScopeSupervisorJob 的异常隔离

在父子协程关系中,异常会默认影响整个作用域中的其他协程。然而,supervisorScopeSupervisorJob 可以帮助实现异常隔离,使一个子协程的失败不会导致其他协程取消。

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 是用于捕获未处理异常的全局处理器。
  • supervisorScopeSupervisorJob 用于隔离异常,使一个协程的失败不会影响其他协程。
  • CancellationException 表示协程的取消,一般不向上层传播。