协程速通 · Android

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)                         // 两者都完成才组装
    }
}
顺序 vs 并行

如果写成 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() 才执行。本想「懒一点」结果变成「永远不执行」,是常见疏忽。

Kotlin 协程速通 · Android 开发 · 内容基于 kotlinx.coroutines 1.7+