1. 核心差异一览
维度 | launch | async |
---|---|---|
返回类型 | Job(无结果) | Deferred(Job 的子类型,有结果) |
获取结果 | 无;只能 join() 等它结束 | await() 拿 T(或抛出异常) |
异常抛出的时机 | 立即 向上冒泡: - 若是根协程 :交给 CoroutineExceptionHandler(或默认报错); - 若在 coroutineScope:立刻取消父作用域,父作用域最终抛出该异常 | 延迟到消费结果时 :异常被封存在 Deferred ,在 await() 时抛出;但→ 在结构化并发里同样会立刻取消父作用域(见 §2) |
CoroutineExceptionHandler | 能处理 launch 的未捕获异常(尤其根协程) | 对 async 无效(异常被当作结果的一部分保存,只有 await 才抛) |
只等待不取结果 | job.join() 仅等待完成;发生失败通常表现为 CancellationException(原因是实际异常) | deferred.join() 也只等待;不会抛原始异常,拿原始异常要用 await() 或 getCompletionExceptionOrNull() |
口诀:要结果用 async/await;要"开火忘返"用 launch。
launch 的异常"见光死"(马上上抛);async 的异常"装进盒子"(await 再打开),但依然会取消父作用域。
2. 结构化并发中的异常传播
2.1coroutineScope {}(默认父子关联)
-
子协程(不论 launch 还是 async)一旦失败:
- 立即取消 父作用域与其兄弟;
- 父作用域最终抛出首个异常 (其余作为 suppressed 附着)。
-
对 async:即使你没有 await ,它失败后也会立刻把整个 coroutineScope 推向失败(常见现象:后续 delay/withContext 被取消)。
kotlin
suspend fun demo() = coroutineScope {
val d = async { error("boom") }
// 这里就会被取消(不等你 await)
delay(1000) // ← 立刻取消
// 退出 scope 时抛 "boom"
}
2.2
supervisorScope {}/SupervisorJob
- 子失败不会取消兄弟和父;
- 对 async:异常仍封在 Deferred 里,只有 await 才会抛 ;如果你从不 await,异常会"沉没在 Deferred 里"(但 deferred.isCompleted 为真,getCompletionExceptionOrNull() 可见)。
css
supervisorScope {
val a = async { error("A") } // 不会波及 b
val b = async { 42 }
// ... 此处仍可继续
// 想感知 A 的失败:a.await() / a.getCompletionExceptionOrNull()
}
2.3 根协程(无父 Job)
- GlobalScope.launch { ... } / viewModelScope.launch { ... }(顶层):未捕获异常交给 CoroutineExceptionHandler 或默认处理(日志/崩溃)。
- GlobalScope.async { ... }:没有 await() 就没有异常报告 (静默失败)。因此禁止"裸 GlobalScope.async 不 await"。
3.
join/await/awaitAll/joinAll行为
-
job.join():只等结束;若失败,多数场景得到 CancellationException(其 cause 才是根因)。
-
deferred.await():
- 成功 → 返回 T;
- 失败 → 直接抛出原始异常(不是 CE 包装)。
-
awaitAll(d1, d2, ...):
- 任一失败 → 立刻 取消其他 Deferred 并抛出首个异常(其余压到 suppressed)。
-
joinAll:只等待全部完成;不会把原始异常抛给你。
想"并发+失败尽早"的结果收集 → 用 awaitAll;
想"并发但不因一个失败中断汇总",在 async 内部 try/catch 包装成成功/失败的 Result,再统一处理。
4. 推荐用法(套路)
4.1 并发取结果(标准答案)
kotlin
suspend fun load(): Pair<User, Feed> = coroutineScope {
val u = async { api.user() }
val f = async { api.feed() }
// 任何一个失败都会取消另一个并抛异常
u.await() to f.await()
}
4.2 半容错并发(不想"一死全死")
kotlin
suspend fun loadTolerant(): Pair<Result<User>, Result<Feed>> = supervisorScope {
val u = async { runCatching { api.user() } }
val f = async { runCatching { api.feed() } }
u.await() to f.await() // 不会互相取消,统一返回 Result
}
4.3
launch捕获与上报
scss
val handler = CoroutineExceptionHandler { _, e -> log(e) }
scope.launch(handler) {
try {
work()
} catch (e: Known) {
handle(e) // 业务内消化
} // 其他未捕获异常 → 走 handler
}
4.4 观察
async的失败(不用await也要感知)
scss
val d = async { job() }
d.invokeOnCompletion { e ->
if (e != null) log("async failed", e) // 结构化或监督场景下的监控
}
5. 常见坑
- GlobalScope.async { ... } 不 await:异常静默;CoroutineExceptionHandler 也收不到。
- 只 join 不 await 以为能拿到异常:join 拿不到原始异常,最多一个 CancellationException。
- 在 coroutineScope 里 async 失败还以为不会影响别人:会的!除非用 supervisorScope。
- 把 CoroutineExceptionHandler 用在 async:没用,await 才会抛;要么 try/catch await,要么 invokeOnCompletion/getCompletionExceptionOrNull()。
- 忘记取消未使用的 Deferred:如果业务分支不再需要结果,记得 deferred.cancel(),减少无谓计算与资源占用。
6. 速记清单
- 要结果 → async/await;只做事→ launch。
- launch 异常立即 冒泡;async 异常在 await 抛 ,但仍会取消父作用域(非监督)。
- 需要"彼此独立"→ supervisorScope 或 SupervisorJob。
- 汇总并发结果:awaitAll(失败即抛);容错:Result + supervisorScope。
- 根协程:launch 异常走 CoroutineExceptionHandler;async 必须 await 才能看到异常。