Skip to content

作用域失败传递深度解析

在结构化并发中,理解异常如何传播也就是理解生与死 (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() } // 这里崩了没事

    }

}