协程不是轻量线程,是一棵责任树
真正值钱的不是能开很多任务,而是每个任务都有父母、寿命、失败传播与收尸的人。
并发上传三张相互独立的图片,其中一张失败。产品要求另外两张继续。最贴合语义的是:
GlobalScope.launch三次- 普通
coroutineScope内async三次 supervisorScope内各自捕获结果- 把所有异常都吞掉
父任务为孩子负责
结构化并发要求新协程挂在某个 CoroutineScope 下。父任务完成前等待孩子;父任务取消时递归取消孩子;普通父子关系里,一个孩子失败会让父任务失败,并取消兄弟。这让「页面离开后搜索请求还在跑」从手工清理问题变成结构本身的性质。
viewModelScope 把任务寿命绑到 ViewModel;lifecycleScope 绑到 LifecycleOwner;LaunchedEffect 绑到 Composition 中那次调用。选 scope 的本质是在回答:谁有权宣布这项工作不再需要?
失败传播取决于业务原子性
如果头像、姓名、执业信息必须作为一份完整资料一起加载,任何一项失败都应让整个结果失败,普通 coroutineScope 合理。如果三张附件彼此独立,一张失败不该取消另外两张,使用 supervisorScope 或由更上层分别管理结果。
SupervisorJob 不是「永不失败」。它只是阻止子任务失败自动向兄弟横向扩散;失败仍要被观察、转成状态或上报。CoroutineExceptionHandler 也不是通用 try/catch:它只处理根协程中未捕获的异常,无法让已经失败的协程恢复。
suspend fun uploadAll(items: List<Attachment>): List<UploadResult> =
supervisorScope {
items.map { item ->
async {
try {
UploadResult.Done(item.id, upload(item))
} catch (cancelled: CancellationException) {
throw cancelled
} catch (error: Throwable) {
UploadResult.Failed(item.id, error)
}
}
}.awaitAll()
}
取消是合作,不是强杀
挂起函数通常会检查取消并抛出 CancellationException。CPU 密集循环没有挂起点时,要周期性调用 ensureActive() 或 yield()。捕获 Throwable 时若把取消异常一起吞掉,父任务以为孩子已取消,孩子却继续工作。
finally 仍会运行,适合释放资源。只有极短、确实必须完成的清理才用 withContext(NonCancellable);拿它包业务上传,相当于撕掉作用域的取消合同。
launch 的未捕获异常向父级传播;async 把结果放进 Deferred,但在结构化父子关系中失败仍会取消父级,不能靠「不 await」假装没发生。局部恢复用 try/catch,兄弟隔离用 supervisor。比较普通作用域与监督作用域。下面这个版本会保留第二个孩子的完成结果:
first=failed second=completed parent=completed with partial results
import kotlinx.coroutines.*
fun main() = runBlocking {
supervisorScope {
val first = async {
try {
delay(10)
error("upload failed")
} catch (error: IllegalStateException) {
"failed"
}
}
val second = async {
delay(30)
"completed"
}
println("first=${first.await()}")
println("second=${second.await()}")
println("parent=completed with partial results")
}
}在 Android Studio Kotlin/JVM scratch 运行。再让第一个 async 不捕获异常,并把 supervisorScope 改成 coroutineScope,观察失败为何会取消兄弟。
聊天页可能同时收集消息、在线状态和正在输入提示。它们常常可以独立失败;而「先刷新令牌,再用新令牌提交消息」是严格依赖链。别按技术类型统一选择 supervisor,要按产品原子性决定失败边界。
「GlobalScope 能避免页面取消任务。」它只是让任务失去明确所有者,测试、注销、账号切换与进程内资源清理都会变难。
若工作要比页面活得久,给它一个真实的更长寿命所有者;若要跨进程,持久化意图并交给 WorkManager,而不是制造孤儿协程。
答案是第 3 项。三张图片独立,监督作用域能隔离兄弟失败;每个任务仍把异常变成可见结果,同时重新抛出取消。普通 coroutineScope 适合全成或全败,GlobalScope 丢失所有权,吞异常则丢失故障证据。
这一章的一句话
先定义谁跟谁共命运,再选择 coroutineScope 还是 supervisorScope。
下一章让任务开始持续产出值:同一个 Room 查询被三个界面收集时,会执行一次还是三次?答案取决于它是冷流、热流,还是被 stateIn 接上了共享电源。