协程async vs launch 的异常与结果学

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)一旦失败

    1. 立即取消 父作用域与其兄弟
    2. 父作用域最终抛出首个异常 (其余作为 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. 常见坑

  1. GlobalScope.async { ... } 不 await:异常静默;CoroutineExceptionHandler 也收不到。
  2. 只 join 不 await 以为能拿到异常:join 拿不到原始异常,最多一个 CancellationException。
  3. 在 coroutineScope 里 async 失败还以为不会影响别人:会的!除非用 supervisorScope。
  4. 把 CoroutineExceptionHandler 用在 async:没用,await 才会抛;要么 try/catch await,要么 invokeOnCompletion/getCompletionExceptionOrNull()。
  5. 忘记取消未使用的 Deferred:如果业务分支不再需要结果,记得 deferred.cancel(),减少无谓计算与资源占用。

6. 速记清单

  • 要结果 → async/await;只做事→ launch。
  • launch 异常立即 冒泡;async 异常在 await 抛 ,但仍会取消父作用域(非监督)。
  • 需要"彼此独立"→ supervisorScope 或 SupervisorJob。
  • 汇总并发结果:awaitAll(失败即抛);容错:Result + supervisorScope。
  • 根协程:launch 异常走 CoroutineExceptionHandler;async 必须 await 才能看到异常。
相关推荐
今禾4 小时前
深入解析CSS Grid布局:从入门到精通
前端·css·面试
卷福同学5 小时前
#去深圳了~
后端·面试·架构
绝无仅有5 小时前
面试MySQL基础20题(一)
后端·面试·github
白露与泡影8 小时前
2025年高质量Java面试真题汇总
java·python·面试
꒰ঌ 安卓开发໒꒱8 小时前
Java 面试 -Java基础
java·开发语言·面试
讨厌吃蛋黄酥14 小时前
🔥 面试必考题:手写数组扁平化,5种方法全解析(附代码+图解)
前端·javascript·面试
GISer_Jing14 小时前
作业帮前端面试(准备)
前端·面试·职场和发展
倔强青铜三15 小时前
苦练Python第52天:一口气吃透Python的“七脉神剑”:生命周期+字符串魔术方法全解析
人工智能·python·面试
在未来等你19 小时前
Elasticsearch面试精讲 Day 19:磁盘IO与存储优化
大数据·分布式·elasticsearch·搜索引擎·面试