一句话总结
coroutineScope→ “连坐制” :一个子协程崩溃,所有兄弟协程陪葬,父协程也取消。supervisorScope→ “责任隔离” :一个子协程崩溃,其他兄弟协程继续干活,父协程不受影响。
一、核心区别:异常传播与任务管理
coroutineScope 和 supervisorScope 都是协程作用域构建器,它们创建了一个新的子协程作用域。但它们在处理子协程的异常和取消时,行为截然不同。
coroutineScope | supervisorScope | |
|---|---|---|
| 异常传播 | 子协程的异常会向上传播。父协程收到异常后会取消自己,并递归地取消所有其他子协程。 | 子协程的异常不会向上传播。父协程不受影响,其兄弟协程可以继续运行。 |
| 任务管理 | 除非所有子协程都成功完成,否则 coroutineScope 不会完成。它提供了原子性保证。 | 子协程之间相互独立。supervisorScope 只负责管理自身的生命周期,不干预子协程的失败。 |
| 典型用途 | 多个任务需要**“全有或全无”**的语义,如数据库事务、复杂的业务流程。 | 多个任务相互独立,一个失败不应影响其他任务,如并发的网络请求。 |
二、实践中的选择:何时使用哪种作用域?
选择正确的作用域构建器,是编写健壮、可维护的协程代码的关键。
1. coroutineScope:原子性的守护者
coroutineScope 适用于那些一个任务失败就意味着整个任务失败的场景。
-
示例:用户转账操作
- 任务A:从账户A扣款
- 任务B:向账户B存款
- 如果任务A失败,
coroutineScope会确保任务B不会被执行,从而避免数据不一致。
suspend fun transferMoney() = coroutineScope {
launch { deductFromAccountA() }
launch { addToAccountB() } // 如果上一步失败,这一步会被取消
}
2. supervisorScope:独立任务的管理者
supervisorScope 适用于那些任务之间没有依赖的场景。一个任务的失败不应该影响其他任务。
-
示例:加载主页数据
- 任务A:加载用户信息
- 任务B:加载推荐列表
- 任务C:加载广告
- 如果广告加载失败,用户和推荐列表仍然可以正常显示。
suspend fun loadHomepageData() = supervisorScope {
launch { fetchUser() } // 如果失败,不影响其他任务
launch { fetchRecommendations() }
launch { fetchAds() }
}
三、关键细节:异常处理与 CoroutineExceptionHandler
supervisorScope 的“责任隔离”并不意味着异常会被自动处理。未被捕获的异常仍然会从协程的根部抛出,如果无人处理,会导致应用崩溃。
-
coroutineScope 的异常处理:
子协程的异常会传播到 coroutineScope 的外部,因此我们通常在外部使用 try-catch 来捕获和处理异常。
-
supervisorScope 的异常处理:
子协程的异常不会向上传播,因此我们需要在子协程内部使用 try-catch 或在 CoroutineContext 中添加一个 CoroutineExceptionHandler 来处理。
// 使用 CoroutineExceptionHandler 处理 supervisorScope 中的异常
val handler = CoroutineExceptionHandler { _, exception ->
println("在 supervisorScope 中捕获到异常: $exception")
}
suspend fun doTasks() = supervisorScope {
launch(handler) {
throw RuntimeException("任务B炸了!") // ✅ 异常被handler捕获,不影响其他任务
}
launch {
delay(2000)
println("任务C正常执行")
}
}
四、结论
coroutineScope 是严格模式下的协程作用域,提供原子性保证。而 supervisorScope 则是佛系模式下的协程作用域,提供健壮性和独立性。理解并正确运用这两个作用域构建器,是编写高效、安全的 Kotlin 协程代码的关键。