作用域失败传递深度解析
在结构化并发中,理解异常如何传播也就是理解生与死 (Cancellation & Failure) 的规则。
两大传播流向
异常传播并非单一方向,而是分为“异常上抛”和“取消下发”。
1. 默认:双向传播 (coroutineScope / launch)
标准的父子协程关系遵循“连坐制度”。
- 子协程 -> 父协程: 子协程崩溃,将异常抛给父协程。
- 父协程 -> 其它兄弟: 父协程收到异常,为了响应“整体结构完整性”,它会取消自己以及所有其他子协程。
- 父协程 -> 爷爷: 最后,父协程将异常继续向上抛出。
kotlin
coroutineScope {
// 子协程 A 失败
launch { throw Error() }
// 子协程 B 会被取消,抛出 CancellationException
launch {
delay(1000)
println("I will never print")
}
}2. 监督:单向传播 (supervisorScope / SupervisorJob)
用于“兄弟爬山,各自努力”的场景。
- 子协程 -> 父协程: 子协程崩溃,父协程忽略该崩坏,不取消自己。
- 父协程 -> 其它兄弟: 由于父协程未取消,兄弟协程继续存活。
- 子协程自身: 必须自行处理异常(如
CoroutineExceptionHandler),否则在 Android 上会导致应用 Crash。
特殊异常:CancellationException
它是协程间沟通取消信号的信使。
- 特性: 抛出
CancellationException被视为“正常结束”而非“失败”。 - 传播: 父协程收到它,只认为是该子协程取消了,不会进而取消其他兄弟。
最佳实践
如果你想取消一个协程,请始终使用 job.cancel() 或抛出 CancellationException。切勿抛出 RuntimeException 来试图取消,那会导致父级崩溃。
陷阱:SupervisorJob 必须作为直接父亲
SupervisorJob 只有在它直接作为父 Job 时才生效。最常见的错误是将它作为参数传给 launch。
kotlin
// ❌ 错误用法
val scope = CoroutineScope(Dispatchers.Main)
// 这里的 SupervisorJob 变成了 launch 所创建的新 Job 的“父级”
// 而 launch 本身创建的 Job 依然是普通的 Job (非 Supervisor)
scope.launch(SupervisorJob()) {
// 因此,这里抛出异常,依然会导致父 scope (及其下的所有协程) 被取消!
launch { throw Error() }
}
// ✅ 正确用法 1: 在 Scope 构造时传入
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
// ✅ 正确用法 2: 使用 supervisorScope 作用域构建器
scope.launch {
supervisorScope {
launch { throw Error() } // 这里崩了没事
}
}