短答:默认会立刻取消。
在同一个(非监督的)协程作用域里,任意一个 async 失败(抛出未捕获异常)会取消父作用域,其兄弟协程随即被取消;它们会在下一个可取消挂起点(如 delay/IO/emit/send)收到 CancellationException 并结束。
三种常见语义 & 正确写法
1) 默认语义:一失败,全部取消(fail fast)
suspend fun failFast() = coroutineScope {
val a = async { workA() }
val b = async { workB() } // 若这里抛异常
val c = async { workC() } // ← 会被取消
awaitAll(a, b, c) // 抛出 b 的异常
}
-
适合要么都成功,要么全失败的事务式场景。
-
注意:async 的异常存储在 Deferred 里并在 await/awaitAll 时抛,但取消信号会立刻向兄弟传播。
2) 想让其他任务继续完成:用supervisorScope(监督作用域)
suspend fun keepOthersRunning() = supervisorScope {
val a = async { workA() }
val b = async { workB() } // 失败不会取消 a/c
val c = async { workC() }
// 别用 awaitAll(a,b,c) 直接把异常抛出作用域(会导致取消);分别处理:
val ra = runCatching { a.await() }
val rb = runCatching { b.await() } // 失败被吃掉
val rc = runCatching { c.await() }
listOf(ra, rb, rc).mapNotNull { it.getOrNull() }
}
-
监督作用域下,子协程失败不会取消兄弟;但若你把异常向外抛出 supervisorScope,它也会取消所有子协程。所以在作用域内部消化异常。
3) 想“收集成功、忽略失败”:结果即语义
suspend fun collectSuccesses() = supervisorScope {
val jobs = tasks.map { t -> async { runCatching { doWork(t) } } }
jobs.awaitAll().mapNotNull { it.getOrNull() } // 成功的留下
}
细节补充
- 取消是协作式:CPU 密集代码若没有挂起点或 yield()/isActive 检查,响应取消会滞后。
- launch vs async:两者子协程失败都会取消父作用域;区别在于异常传播方式(launch 立即向上报;async 在 await 时抛)。
- 想手动策略:拿到 Deferred 列表后,你也可以在捕获到某个失败时显式 others.forEach { it.cancel() } 或相反地继续 await 其他。
速记
- 默认(coroutineScope) :一挂全挂(fail fast)。
- 监督(supervisorScope/SupervisorJob) :彼此独立,异常要在作用域里消化才能让其他继续。
- 响应取消取决于挂起点:加 delay/IO/yield/isActive 让任务“能被停得下来”。