协程启动基础:launch与async的区别、Job与Deferred

刚开始接触Kotlin协程时,我老把 launch 和 async 搞混。说白了,它俩就像家里的两把螺丝刀——一把是普通平口的,一把是十字花的。虽然都能拧螺丝,但用错了地方就会出事。这章我们就彻底拆开它们,顺便把 Job 和 Deferred 也讲明白。

1. 启动协程的两大工具:launch 和 async

先看张对比表,心里有个底:

对比项 launch async
返回值 Job Deferred<T>
是否返回结果 ❌ 不返回 ✅ 通过 await() 返回
典型场景 发起一个任务,不需要结果(如日志、UI更新) 需要异步计算结果(如网络请求、数据库查询)
异常传播 抛出到父协程(默认立即取消) await() 时才会抛出异常

代码看看就明白

// launch:只管发射,不求回报
fun main() = runBlocking {
    val job: Job = launch {
        delay(1000)
        println("launch 任务完成")
    }
    job.join()  // 等待完成
    println("launch 执行结束")
}

// async:发射并等待结果
fun main() = runBlocking {
    val deferred: Deferred<String> = async {
        delay(1000)
        "async 的结果"
    }
    val result = deferred.await()  // 阻塞等待结果
    println(result)
}

💡 我个人的习惯是:如果函数签名里不需要返回值,就用 launch。一旦你看到调用方需要拿结果,立刻换成 async。记住一个口诀——“无结果 launch,有结果 async”。

为什么要区分得这么清楚?好比你开车。launch 就像一脚油门踩下去,车跑了,你也不关心它跑到了哪。async 则像你叫了个外卖——你发起订单,然后等着骑手把饭送到手里。你可以在这期间干别的(比如看剧),但最终你得 await() 才能拿到盒饭。这两种场景在项目里比比皆是。

2. Job 与 Deferred:返回值与状态管理

launch 返回的是 Job,async 返回的是 Deferred<T>。它们到底是什么关系?

  • Job:一个可取消的协程生命周期句柄。你能用 cancel()、join() 来控制它,还能通过 isActive 检查它是否还在跑。
  • Deferred:它继承自 Job,额外多了一个 await() 方法。说白了,Deferred 就是一个带返回值、且可以“等待”的 Job。

状态迁移

// Job 的状态机(简化版)
// New ——> Active ——> Completing ——> Completed
//                ↳ Cancelling ——> Cancelled

// Deferred 同理,只是完成时带着结果

我去年在一个下载管理模块里踩过一个坑:用 async 发起十几个请求,然后全部 await(),结果有个请求抛异常,整个协程树都炸了。为什么呢?因为 async 默认的异常处理方式和 launch 不一样——它把异常包在了 Deferred 里,等你 await() 时才会抛。如果不小心没捕获,异常就会沿着协程作用域向上扩散,导致兄弟协程也殉葬。

⚠️ 特别注意:await() 会抛出被 async 包裹的异常。如果你在 await() 外面没做 try-catch,一旦某个 async 任务失败,整个调用链可能崩掉。我曾经就是忘了处理这个,导致用户看到的下载进度条突然闪退。

来看一个对比表格:

属性/方法 Job Deferred<T>
继承关系 根接口 继承自 Job
获取结果 无 suspend fun await(): T
取消控制 cancel() / join() 同上(继承自 Job)
是否需要等待结果 否(通常用 join 等待完成) 是(必须 await 获取值)

这里有个容易犯的错:很多人在用 async 时,直接调 async { ... }.await(),这其实和串行没区别——你想想,你发起一个任务然后立刻阻塞等它,那干吗不直接用 withContext 或者同步代码?正确的做法是:先 async 创建多个任务,然后再统一 await,这样才能并行。

// ❌ 错误的串行写法
val result = async { fetchData() }.await()

// ✅ 正确的并行写法
val deferred1 = async { fetchUser() }
val deferred2 = async { fetchConfig() }
val user = deferred1.await()
val config = deferred2.await()

3. 实战中的选择建议

写代码时怎么快速决定?我总结了三个判断点:

  1. 要不要结果? → 要就 async,不要就 launch。
  2. 需不需要等待它完成再做下一步? → 需要的话用 join() 或 await();如果完全独立,直接 launch 不用管。
  3. 是否想让多个任务并行后汇总? → 用 async 启动一堆 Deferred,然后 awaitAll() 或逐条 await()。

🎯 我个人特别喜欢的一个模式:在 ViewModel 里用 viewModelScope.launch 启动一批并行网络请求,其中需要返回值的用 async,不需要的(如埋点上报)用 launch。这样代码读起来特别清晰——每个协程的意图一目了然。

你可能会问:launch 返回的 Job 有啥用?如果你需要取消任务或者监测状态,Job 就是你的遥控器。而 Deferred 等于这个遥控器上多了一个“显示当前外卖位置”的屏幕。

🔧 一个小技巧:如果你只是想启动一个协程,既不关心结果也不需要取消,连 Job 变量都可以不存。直接 launch { ... } 就完事。不过,在大型项目里我建议还是显式持有 Job,方便后续排查是否泄漏。

章节知识体系:launch / async 与 Job / Deferred 的关系图

launch vs async 与 Job / Deferred 的关系 协程启动 launch async Job Deferred<T> 状态控制 / 取消 await() 获取结果 无返回,只管执行 适用:埋点、UI更新 带返回,需要 await 适用:网络请求、计算

💡 看这张图你就理解了:左边是 launch 的职责链——只管启动,通过 Job 控制生命周期;右边是 async 的职责链——启动并期待结果,通过 Deferred 的 await 取出值。两条线泾渭分明,但在上层设计时经常会交叉使用。比如你可以在一个 coroutineScope { } 里同时用 launch 做日志,用 async 做接口请求——完全没问题。

最后说一句:不要滥用 async。很多人有“万物皆 async”的倾向,觉得带返回值就是好的。我见过一个项目,连保存一个布尔值都用 async,结果 await() 一大堆,性能反而下降了。launch 和 async 各有各的主场——该安静的时候别踢球,该踢球的时候别站着。