一句话总结
launch()→ “只管干活,不问结果” (适合不需要返回值的任务,比如发日志)。async()→ “干活完记得交结果” (适合需要返回值的任务,比如并发请求数据)。
一、核心区别:返回值与异常处理
launch 和 async 都是启动协程的构建器,但它们在设计哲学上有着根本的区别,这主要体现在返回值和异常处理上。
launch() | async() | |
|---|---|---|
| 返回值 | 返回 Job,代表协程的生命周期和状态(如是否完成、是否取消),但没有结果。 | 返回 Deferred<T> ,它继承自 Job,并额外提供了一个 await() 方法来获取结果 T。 |
| 异常处理 | 异常会立即传播给父协程,如果父协程没有处理,会取消父协程及其所有子协程。 | 异常会被封装在 Deferred 对象中,只有在调用 await() 方法时才会被抛出。如果 await() 从未被调用,异常可能会被静默忽略。 |
| 典型场景 | 适用于**“消防即走”**的任务,如上传日志、发送网络请求(不关心响应)、更新UI。 | 适用于需要**“并发”执行并“汇总结果”**的任务,如同时请求多个API并合并数据。 |
二、实践案例:并发任务与结构化并发
async() 的真正力量在于其与结构化并发的结合。它允许我们以一种优雅、安全的方式来并行执行多个任务。
1. 并发与依赖:
async 可以在同一个协程作用域内启动多个任务,然后通过 await() 来等待它们的完成。
suspend fun fetchDataAndShow() {
val deferredUser = async { fetchUserData() }
val deferredPosts = async { fetchUserPosts() }
// 任务并发执行,直到这里才等待结果
val user = deferredUser.await()
val posts = deferredPosts.await()
// 将两个结果组合处理
showData(user, posts)
}
三、异常处理的陷阱与最佳实践
launch 和 async 不同的异常处理机制,是开发者最常遇到的陷阱。
-
launch 的“失败即全部”:
当一个 launch 协程抛出未捕获的异常时,它会立刻传播给其父协程,导致整个任务树被取消。这种行为模式适用于那些所有子任务都必须成功的场景。
-
async 的“静默异常”:
如果 async 任务抛出异常,而你从未调用 await(),这个异常将永远不会被抛出。这可能导致难以调试的问题。因此,在使用 async 时,必须确保你的代码会调用 await() 来处理潜在的异常。
四、高级用法:CoroutineScope与异常处理
在实际项目中,我们通常会使用 CoroutineScope 来管理协程的生命周期和异常处理。
viewModelScope:Android 的viewModelScope默认使用SupervisorJob。这意味着一个子协程的失败不会影响其他子协程。在viewModelScope中,launch抛出的异常会立即取消自身,而async抛出的异常则需要通过await()来处理。CoroutineExceptionHandler:作为最后的防线,你可以使用CoroutineExceptionHandler来处理所有未被捕获的异常,从而防止应用崩溃。
五、结论:如何选择?
- 如果你的任务是一个**“发射并遗忘”**的后台操作,无需任何返回值,请使用
launch()。 - 如果你需要并发执行多个任务,并需要它们的返回值来进一步处理,请使用
async()并确保调用await()来获取结果和处理异常。