launch 和 async 的区别是什么?何时应该使用其中一个?

解读

国内面试场景下,这道题几乎 100% 出现在“Kotlin 协程”环节,面试官想确认三件事:

  1. 你是否真的用过协程,而不是只会背“launch 是 Fire-and-forget、async 是带返回值”这种口诀;
  2. 能否结合 Android 真实生命周期(Activity/Fragment/ViewModel/Service)讲出“结构化并发 + 作用域”的落地细节;
  3. 能否区分“并发性能”与“数据竞争”场景,给出线程安全、异常处理、取消传播、主线程切换的完整闭环。
    答得太浅(只背定义)会被追问“那异常怎么收?”“返回值怎么等?”;答得太深(直接上 Mutex、Semaphore、Channel)又容易被判“过度设计”。因此要把“区别、场景、踩坑、最佳实践”四层讲透,但控制在 2 分钟内,让面试官听到关键词:结构化并发、CoroutineScope、await、try-catch、SupervisorJob、withContext。

知识点

  1. 协程构建器:launch 返回 Job,async 返回 Deferred(Job 子类)。
  2. 结构化并发:两者都必须落在 CoroutineScope 里,取消与异常沿作用域树传播。
  3. 异常策略:launch 异常默认向上抛给父作用域,async 只在 await() 调用处抛出;SupervisorJob 可隔离异常。
  4. 返回值:launch 无结果,async 可通过 await() 挂起获取结果;await() 会重新抛出异常,需包裹 try-catch。
  5. 并发性能:async 适合“多任务并行后聚合”场景,可配合 coroutineScope{} 做并发 map-reduce;launch 适合“后台持续任务”或事件驱动副作用。
  6. Android 生命周期:ViewModel 里推荐 viewModelScope,UI 层推荐 lifecycleScope;两者默认主线程,IO/CPU 任务需 withContext 切换。
  7. 取消传播:UI 销毁时作用域自动取消,await() 若未调用则 async 异常被静默丢弃,需确保在 try-finally 中清理资源。
  8. 性能陷阱:async(Dispatchers.Default) 创建过多并发任务会抢占线程池,导致帧率抖动;launch 开启大量无阻塞任务同样会撑爆默认线程池,需限流或自定义线程池。
  9. 测试角度:launch 异常在 TestScope 里可用 testScheduler 抛出;async 异常需显式 await() 才能在单元测试里断言。
  10. 国内特有:部分厂商 ROM 对后台线程存活时间有限制,launch 做长轮询时必须加前台 Service,否则 1 分钟被系统杀进程。

答案

一句话区别:launch 用于“不需要返回值的副作用任务”,async 用于“需要并行计算后拿结果”的场景。
使用准则:

  1. 只在需要并发提速且后续必须聚合结果时用 async,其他一律 launch;
  2. 调用 async 后必须保证后续会 await(),否则异常会静默丢失;
  3. 在 Android 中,永远把两者放到生命周期作用域(viewModelScope / lifecycleScope),禁止 GlobalScope;
  4. 若并发任务彼此独立且允许部分失败,用 supervisorScope + async,并在 await() 时逐个捕获异常;
  5. 如果任务只产生 UI 副作用(写数据库、发日志、弹 Toast),直接用 launch,内部再 withContext 切线程,避免过度设计。

示例对比:
并发网络请求再合并——

coroutineScope {
    val deferredUser = async(Dispatchers.IO) { api.getUser() }
    val deferredConfig = async(Dispatchers.IO) { api.getConfig() }
    showUI(deferredUser.await(), deferredConfig.await())
}

后台打点——

lifecycleScope.launch {
    logEvent("page_open")   // 无需返回值
}

拓展思考

  1. 为什么官方不推荐“async 嵌套 launch”?
    答:异常传播路径混乱,launch 内异常会提前取消整个作用域,导致 async 的 await() 抛出 JobCancellationException,难以定位根因。
  2. 在 Repository 层做“缓存+网络”双源并发时,如何既用 async 又避免把异常带到 ViewModel?
    答:在 repository 方法内部使用 supervisorScope,两个 async 分别捕获异常后返回 Result<T> 封装,ViewModel 只拿到结果数据,异常隔离在数据层。
  3. 国内厂商后台限制下,如何用 launch 实现可持续上传日志?
    答:启动前台 Service + 独立 Process,自定义 CoroutineScope(ForkJoinPool + SupervisorJob),在 Service 的 onDestroy 里 scope.cancel(),既保活又可随时释放资源,避免内存泄漏。