launch + join vs. async + await:Kotlin协程的任务协调哲学

219 阅读3分钟

一句话总结

launch + join vs. async + await:Kotlin协程的任务协调哲学

  • launch + join“等一个没有结果的任务完成” (比如等后台任务结束)。
  • async + await“等一个有结果的任务完成,并拿到返回值” (比如等网络请求返回数据)。

一、核心区别:目的与返回值

launchasync 都是协程构建器,但它们的根本区别在于目的返回值

launch() + join()async() + await()
目的等待任务完成launch 启动一个协程,join 是一个挂起函数,它会暂停当前协程,直到 launch 协程完成。等待任务完成并获取结果async 启动一个协程并返回一个 Deferred<T> 对象,await 是一个挂起函数,它会暂停当前协程,直到 Deferred 任务完成并返回结果。
返回值launch 返回 Job,它是一个无返回值的句柄,用于管理协程的生命周期和状态。async 返回 Deferred<T> ,它继承自 Job,并额外提供了一个 await() 方法来获取结果。
典型场景确保任务按顺序执行,或在继续之前等待一个后台操作完成。并发执行多个任务,并需要它们的返回值进行进一步处理。

二、实践对比:顺序与并发

  • launch + join:顺序执行

    当需要确保任务 A 在任务 B 之前完成时,join 是一个简单有效的同步工具。

    // 任务A和任务B需要顺序执行
    suspend fun sequentialTasks() {
        val jobA = launch { taskA() }
        jobA.join() // 挂起当前协程,直到 taskA() 完成
        val jobB = launch { taskB() }
        jobB.join() // 等待 taskB() 完成
    }
    
  • async + await:并发执行

    当多个任务可以同时执行,并且你需要它们的返回值时,async 是实现高效并发的最佳选择。await 只会挂起当前协程,而不是阻塞底层线程。

    // 同时执行 taskA() 和 taskB(),并合并结果
    suspend fun concurrentTasks() {
        val deferredA = async { taskA() }
        val deferredB = async { taskB() }
    
        // 挂起当前协程,直到两个任务都完成
        val resultA = deferredA.await() 
        val resultB = deferredB.await()
    
        // 合并结果
        combineResults(resultA, resultB)
    }
    

三、异常处理的陷阱

launchasync 在异常传播方面存在关键差异,这使得它们在错误处理方面各有优劣。

  • launch 的即时传播:

    launch 协程中抛出的未捕获异常会立即传播给父协程。如果父协程没有处理,它会取消父协程及其所有子协程。这使得错误能被快速发现,但可能导致“全盘皆输”。

  • async 的延迟处理:

    async 协程中抛出的异常会被封装在 Deferred 对象中,并暂存起来。只有在调用 await() 方法时,这个异常才会被抛出。这提供了更灵活的异常处理时机,但如果 await() 从未被调用,异常将永远不会被抛出,从而导致难以调试的**“异常静默”**。


四、结论:如何选择?

  • 选择 launch + join:当你需要启动一个后台任务,并且在不关心其返回值的情况下,确保它在你的主流程继续之前完成时。
  • 选择 async + await:当你需要并发执行多个任务,并且需要它们的返回值来进行进一步处理时。这是 Kotlin 协程实现并发编程的黄金搭档。

记住:能并发就别顺序。在大多数场景下,async + await 提供了更高的效率和更好的用户体验。