8. 异步并发 async/await
async 启动一个会返回结果的协程,得到 Deferred<T>,调用 await() 挂起取值。关键:async 一调用就立刻开始执行,await() 才是「等它完成」。
一、全家福(按使用频率)
| 频率 | API | 是什么 | 典型场景 |
|---|---|---|---|
| 🔥 极高 | async { ... } | 启动并发任务,返回 Deferred | 并行请求多个接口、并行计算 |
| 🔥 极高 | deferred.await() | 挂起等待结果 | 合并多个异步结果 |
| 🔥 极高 | coroutineScope { ... } | 开子作用域、全部完成/任一失败才返回 | 在 suspend 函数里组织并行 |
| 高 | awaitAll(d1, d2, ...) | 等所有 Deferred 完成,聚合成 List | 不定数量的并发任务汇总 |
| 高 | launch { ... } | 点火不返回值(对比用) | 只管触发、不要结果 |
| 中 | async(start = CoroutineStart.LAZY) { ... } | 延迟启动,调用 await/start 才跑 | 结果可能用不上的「懒」并发 |
| 中 | supervisorScope { ... } | 并行但子任务互不连坐 | 多个独立任务,一败不拖垮其他 |
| 低 | Deferred.cancel() | 单独取消某个并发任务 | 某个子结果不再需要 |
二、并行请求(ViewModel)
下面写在 ViewModel 的方法中,数据请求来自 Repository。两个接口互相独立,应当并行而非串行。
class UserViewModel(private val repo: UserRepository) : ViewModel() {
// 多个独立请求并行:用 async/await(写在 ViewModel 方法里)
fun loadDashboard() = viewModelScope.launch {
val user = async { repo.getUser() } // 立刻开始
val orders = async { repo.getOrders() } // 立刻开始(与上面同时)
val profile = user.await() // 等用户
val list = orders.await() // 等订单(通常已就绪)
show(profile, list) // 两者都完成才组装
}
}
如果写成 val a = repo.getUser(); val b = repo.getOrders()(去掉 async),那就是先等 A 再等 B,总耗时相加。加上 async 后总耗时取二者最大值,列表页、详情页、首页聚合数据都能明显提速。
三、用 coroutineScope 封装并行(Repository / UseCase)
把「并行取数」收敛成一个 suspend 函数,放在 Repository 或 UseCase(不要放 ViewModel,那是业务逻辑层的事)。coroutineScope 保证两个子任务都跑在自己作用域里,任一失败整体抛异常。
class DashboardRepository {
// 任一失败 → 整体抛异常,由调用方统一处理
suspend fun loadBoth(): DashboardData = coroutineScope {
val user = async { api.getUser() }
val orders = async { api.getOrders() }
DashboardData(user.await(), orders.await()) // 等价于 user.await() to orders.await()
}
}
// 不定数量的任务:用 awaitAll 汇总
suspend fun loadAll(ids: List<String>): List<User> = coroutineScope {
ids.map { id -> async { api.getUser(id) } }
.awaitAll() // 返回 List<User>,保持顺序
}
四、真实开发中会踩的坑
踩坑 1:async 后不 await,异常被「吞」:async 里的异常会延迟到 await() 才抛出。如果只 async { ... } 却忘了 await,异常永远不会浮现,任务像「静默失败」——等于用 launch 还丢掉了报错能力。要么 await,要么直接用 launch。
踩坑 2:用 launch 当 async,想拿结果却拿不到:launch 返回 Job,拿不到返回值。需要结果就上 async。常见错误:launch { result = compute() } 然后从外部读 result——这是竞态,协程还没跑完主线程就读了。
踩坑 3:coroutineScope 的「连坐取消」:在普通 coroutineScope 里,任一子 async 失败,其余兄弟会被一起取消。如果你希望「A 失败 B 照常」,改用 supervisorScope。这在「部分数据降级展示」时尤其重要(比如用户资料挂了,订单列表还能显示)。
踩坑 4:async 里用了阻塞调用:async(Dispatchers.IO) { Thread.sleep(1000) } 这种还是占着线程。异步任务里该用 delay / 真正的挂起 IO,而不是 Thread.sleep / .blockingGet()。详见并发调度器章节。
踩坑 5:LAZY 模式忘了触发:async(start = CoroutineStart.LAZY) 不会自动开始,必须 await() 或 start() 才执行。本想「懒一点」结果变成「永远不执行」,是常见疏忽。