协程启动基础: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. 实战中的选择建议
写代码时怎么快速决定?我总结了三个判断点:
- 要不要结果? → 要就 async,不要就 launch。
- 需不需要等待它完成再做下一步? → 需要的话用
join()或await();如果完全独立,直接 launch 不用管。 - 是否想让多个任务并行后汇总? → 用 async 启动一堆 Deferred,然后
awaitAll()或逐条await()。
🎯 我个人特别喜欢的一个模式:在 ViewModel 里用 viewModelScope.launch 启动一批并行网络请求,其中需要返回值的用 async,不需要的(如埋点上报)用 launch。这样代码读起来特别清晰——每个协程的意图一目了然。
你可能会问:launch 返回的 Job 有啥用?如果你需要取消任务或者监测状态,Job 就是你的遥控器。而 Deferred 等于这个遥控器上多了一个“显示当前外卖位置”的屏幕。
🔧 一个小技巧:如果你只是想启动一个协程,既不关心结果也不需要取消,连 Job 变量都可以不存。直接 launch { ... } 就完事。不过,在大型项目里我建议还是显式持有 Job,方便后续排查是否泄漏。
章节知识体系:launch / async 与 Job / Deferred 的关系图
💡 看这张图你就理解了:左边是 launch 的职责链——只管启动,通过 Job 控制生命周期;右边是 async 的职责链——启动并期待结果,通过 Deferred 的 await 取出值。两条线泾渭分明,但在上层设计时经常会交叉使用。比如你可以在一个 coroutineScope { } 里同时用 launch 做日志,用 async 做接口请求——完全没问题。
最后说一句:不要滥用 async。很多人有“万物皆 async”的倾向,觉得带返回值就是好的。我见过一个项目,连保存一个布尔值都用 async,结果 await() 一大堆,性能反而下降了。launch 和 async 各有各的主场——该安静的时候别踢球,该踢球的时候别站着。