在 kotlinx.coroutines 项目中,异常的处理和上报遵循结构化并发(Structured Concurrency)的原则。异常并非简单的 try-catch 就能捕获,而是有一套从子协程到父协程层层上报的机制。
1. 异常传播的核心规则
协程构建器分为两类,它们处理异常的方式不同:
- 自动传播(launch / produce):这些构建器将异常视为未捕获异常。它们会立即将异常向上传播给父协程,最终由
CoroutineExceptionHandler或线程的未捕获异常处理器处理。 - 暴露给用户(async / broadcast):这些构建器依赖用户最终消费异常。例如,通过
await()或receive()来捕获。如果未被消费,异常会被封装在Deferred对象中。
2. 异常上报的层层传递过程
当一个子协程抛出非 CancellationException 的异常时,会发生以下过程:
- 子协程失败:子协程捕获到异常。
- 上报父协程:子协程将异常传递给它的父 Job。
- 父协程取消:父协程接收到异常后,会首先取消自身,并同时取消其它的子协程。
- 继续向上传递:父协程继续将异常上报给它的父协程,直到到达根协程(Root Coroutine)。
- 最终处理:一旦到达根协程,异常将通过上下文中的
CoroutineExceptionHandler进行处理。如果没有安装处理器,则由 platform 相关的默认机制处理(如 Android 的应用崩溃或 JVM 的标准错误输出)。
3. 监督机制(Supervision):阻断上报
有时我们不希望一个子协程的失败导致整个协程树崩溃,这时可以使用监督机制:
- SupervisorJob:父协程使用
SupervisorJob时,子协程的失败不会导致父协程取消,也不会影响兄弟协程。异常会直接由子协程尝试在自己的上下文中使用CoroutineExceptionHandler处理(如果存在)。 - supervisorScope:创建一个监督作用域,其内部发生的异常不会向上传播给作用域外的父协程。
4. 异常聚合(Exception Aggregation)
如果多个子协程同时抛出异常:
- 第一个异常胜出:第一个抛出的异常会被上报并处理。
- 后续异常被抑制:在第一个异常之后发生的其它异常会被附加到第一个异常的
suppressed列表中,不会丢失,但也不会触发多次处理逻辑。
5. 总结
在本项目中,try-catch 的有效性取决于你在哪里使用它:
- 在
launch内部:可以捕获并内部消化。 - 在
async的await()调用处:可以捕获该异步任务的异常。 - 在协程外部或父协程级别:通常无法直接通过普通的
try-catch拦截子协程的异常,必须依赖CoroutineExceptionHandler或supervisorScope。