launch 和 async 的区别是什么?何时应该使用其中一个?
解读
国内面试场景下,这道题几乎 100% 出现在“Kotlin 协程”环节,面试官想确认三件事:
- 你是否真的用过协程,而不是只会背“launch 是 Fire-and-forget、async 是带返回值”这种口诀;
- 能否结合 Android 真实生命周期(Activity/Fragment/ViewModel/Service)讲出“结构化并发 + 作用域”的落地细节;
- 能否区分“并发性能”与“数据竞争”场景,给出线程安全、异常处理、取消传播、主线程切换的完整闭环。
答得太浅(只背定义)会被追问“那异常怎么收?”“返回值怎么等?”;答得太深(直接上 Mutex、Semaphore、Channel)又容易被判“过度设计”。因此要把“区别、场景、踩坑、最佳实践”四层讲透,但控制在 2 分钟内,让面试官听到关键词:结构化并发、CoroutineScope、await、try-catch、SupervisorJob、withContext。
知识点
- 协程构建器:launch 返回 Job,async 返回 Deferred(Job 子类)。
- 结构化并发:两者都必须落在 CoroutineScope 里,取消与异常沿作用域树传播。
- 异常策略:launch 异常默认向上抛给父作用域,async 只在 await() 调用处抛出;SupervisorJob 可隔离异常。
- 返回值:launch 无结果,async 可通过 await() 挂起获取结果;await() 会重新抛出异常,需包裹 try-catch。
- 并发性能:async 适合“多任务并行后聚合”场景,可配合 coroutineScope{} 做并发 map-reduce;launch 适合“后台持续任务”或事件驱动副作用。
- Android 生命周期:ViewModel 里推荐 viewModelScope,UI 层推荐 lifecycleScope;两者默认主线程,IO/CPU 任务需 withContext 切换。
- 取消传播:UI 销毁时作用域自动取消,await() 若未调用则 async 异常被静默丢弃,需确保在 try-finally 中清理资源。
- 性能陷阱:async(Dispatchers.Default) 创建过多并发任务会抢占线程池,导致帧率抖动;launch 开启大量无阻塞任务同样会撑爆默认线程池,需限流或自定义线程池。
- 测试角度:launch 异常在 TestScope 里可用 testScheduler 抛出;async 异常需显式 await() 才能在单元测试里断言。
- 国内特有:部分厂商 ROM 对后台线程存活时间有限制,launch 做长轮询时必须加前台 Service,否则 1 分钟被系统杀进程。
答案
一句话区别:launch 用于“不需要返回值的副作用任务”,async 用于“需要并行计算后拿结果”的场景。
使用准则:
- 只在需要并发提速且后续必须聚合结果时用 async,其他一律 launch;
- 调用 async 后必须保证后续会 await(),否则异常会静默丢失;
- 在 Android 中,永远把两者放到生命周期作用域(viewModelScope / lifecycleScope),禁止 GlobalScope;
- 若并发任务彼此独立且允许部分失败,用 supervisorScope + async,并在 await() 时逐个捕获异常;
- 如果任务只产生 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") // 无需返回值
}
拓展思考
- 为什么官方不推荐“async 嵌套 launch”?
答:异常传播路径混乱,launch 内异常会提前取消整个作用域,导致 async 的 await() 抛出 JobCancellationException,难以定位根因。 - 在 Repository 层做“缓存+网络”双源并发时,如何既用 async 又避免把异常带到 ViewModel?
答:在 repository 方法内部使用 supervisorScope,两个 async 分别捕获异常后返回 Result<T> 封装,ViewModel 只拿到结果数据,异常隔离在数据层。 - 国内厂商后台限制下,如何用 launch 实现可持续上传日志?
答:启动前台 Service + 独立 Process,自定义 CoroutineScope(ForkJoinPool + SupervisorJob),在 Service 的 onDestroy 里 scope.cancel(),既保活又可随时释放资源,避免内存泄漏。