launch vs. async:Kotlin协程的任务构建哲学

859 阅读3分钟

一句话总结

  • launch()“只管干活,不问结果” (适合不需要返回值的任务,比如发日志)。
  • async()“干活完记得交结果” (适合需要返回值的任务,比如并发请求数据)。

一、核心区别:返回值与异常处理

launchasync 都是启动协程的构建器,但它们在设计哲学上有着根本的区别,这主要体现在返回值异常处理上。

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)
}

三、异常处理的陷阱与最佳实践

launchasync 不同的异常处理机制,是开发者最常遇到的陷阱。

  • launch 的“失败即全部”:

    当一个 launch 协程抛出未捕获的异常时,它会立刻传播给其父协程,导致整个任务树被取消。这种行为模式适用于那些所有子任务都必须成功的场景。

  • async 的“静默异常”:

    如果 async 任务抛出异常,而你从未调用 await(),这个异常将永远不会被抛出。这可能导致难以调试的问题。因此,在使用 async 时,必须确保你的代码会调用 await() 来处理潜在的异常。


四、高级用法:CoroutineScope与异常处理

在实际项目中,我们通常会使用 CoroutineScope 来管理协程的生命周期和异常处理。

  • viewModelScope:Android 的 viewModelScope 默认使用 SupervisorJob。这意味着一个子协程的失败不会影响其他子协程。在 viewModelScope 中,launch 抛出的异常会立即取消自身,而 async 抛出的异常则需要通过 await() 来处理。
  • CoroutineExceptionHandler:作为最后的防线,你可以使用 CoroutineExceptionHandler 来处理所有未被捕获的异常,从而防止应用崩溃。

五、结论:如何选择?

  • 如果你的任务是一个**“发射并遗忘”**的后台操作,无需任何返回值,请使用 launch()
  • 如果你需要并发执行多个任务,并需要它们的返回值来进一步处理,请使用 async() 并确保调用 await() 来获取结果和处理异常。