六、协程的并行与串行 —— async/await 与结构化组合

灵活组合"并发执行"与"串行等待",通过调度器控制执行线程,用 coroutineScope 实现真正的结构化并发

async await coroutineScope 并行 串行 Deferred 结构化并发

目录导航(点击跳转)

一、并行执行:各自为战的 launch

1两个 launch 同时执行,互不干扰

协程中最直接的并行方式:用多个 launch 分别启动协程,它们各自运行在指定的调度器上,同时执行、互不干扰

// 两个 launch 分别运行在 IO 线程,同时执行
lifecycleScope.launch(Dispatchers.IO) {
    val people = apiService.getUser("yxl")
    // 处理 yxl 的数据...
}

lifecycleScope.launch(Dispatchers.IO) {
    val people = apiService.getUser("syx")
    // 处理 syx 的数据...
}

这两个协程的关系是完全独立的:

  • 它们被提交到 IO 线程池的不同线程(或同一线程的不同时间片)上
  • 谁先完成取决于网络延迟和系统调度,无法预测顺序
  • 一个失败不会影响另一个
launch(IO)
getUser("yxl")
launch(IO)
getUser("syx")

这种方法适合没有依赖关系的独立任务。两个请求的数据互不需要,各自独立处理即可。最典型的场景:首页同时拉取多个不相关的数据源。

二、串行执行:挂起函数的先后顺序

2挂起函数保证先后顺序,线程切换由内部完成

当一个任务依赖前一个任务的结果时,需要串行执行。挂起函数天然支持这种模式:写在后面的代码一定在前面挂起函数恢复之后才执行。

// 使用挂起函数,先请求数据再更新界面
lifecycleScope.launch {
    val people = apiService.getUser("yxl")   // 先获取数据(后台线程)
    showPeopleInfo(people)                    // 再更新界面(主线程)
}

重要认知:挂起函数本身并不自动切换线程。实际线程的切换通常由 apiService 内部通过 withContext(Dispatchers.IO) 完成。挂起只是"暂停",恢复时回到哪个线程取决于调度器的安排。

串行模式的核心优势:后面的代码可以直接使用挂起函数的返回值。后台线程执行的网络请求结果,主线程可以直接拿来刷新界面。

1
launch 在主线程启动协程
2
getUser 内部切到 IO 线程执行网络请求
3
网络请求完成,恢复回主线程
4
showPeopleInfo 在主线程更新界面

串行执行

  • 依赖关系明确,后一步等前一步
  • 代码顺序即执行顺序,可读性高
  • 天然避免竞态条件
  • 适合有依赖的任务链

并行执行

  • 任务独立,同时进行
  • 总耗时 = 最慢的那个任务
  • 无法保证执行顺序
  • 适合无依赖的独立任务

三、async/await:并行与串行的组合

3async 启动有返回值,Deferred 承接结果

如果一个方法需要整合两个网络请求的数据再执行后续操作,单纯用 launch 不够 —— 你需要拿到每个协程的返回值。async 正是为此而生:它启动协程并返回 Deferred,通过 await() 取结果。

先看 async 的源码:

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

launch 源码结构几乎一模一样,区别只在于:launch 返回 Job(无返回值),async 返回 Deferred<T>(有返回值)。Deferred 继承自 Job,所以它既是一个协程句柄,也承载返回值。

4基本用法:async 启动 + await 取结果

lambda 表达式的最后一行就是 async 的返回值:

// lambda 表达式的最后一行就是返回值
val deferred = lifecycleScope.async(Dispatchers.IO) {
    apiService.getPeople("yxl")     // 这行的结果就是 async 的返回值
}

lifecycleScope.launch {
    val badPeople = apiService.getPeople("syx")   // 串行等待这个
    val goodPeople = deferred.await()              // 等待 async 结果
    showPeoplesInfo(badPeople, goodPeople)         // 两者都拿到后展示
}

这种写法实现了两个请求并行执行

  • deferred 在外面提前启动了 async 协程,开始请求 yxl 的数据
  • 里面的 launch 启动后请求 syx 的数据
  • 两个请求同时在进行,而不是一个等另一个
  • deferred.await() 是挂起函数 —— 如果数据还没回来就挂起等待
async(IO)
getPeople("yxl")
launch 内
getPeople("syx")
↓ 并行完成 ↓
await() 拿到结果
拿到结果
showPeoplesInfo
两个结果合并展示

但这种写法不符合结构化并发。async 协程在外部 scope 中游离,如果 launch 内部失败或被取消,外层的 async 不会自动取消,可能造成资源浪费。

四、coroutineScope:结构化并发的正确姿势

5用 coroutineScope 包裹,async 不再游离

coroutineScope 是一个挂起函数,它创建一个新的协程作用域,只有当内部所有协程都完成后才会返回。任何一个子协程失败,都会取消其他子协程。

lifecycleScope.launch {
    val result = coroutineScope {
        val goodPeople = async(Dispatchers.IO) {
            apiService.getPeople("yxl")
        }
        val badPeople = async(Dispatchers.IO) {
            apiService.getPeople("syx")
        }
        Pair(goodPeople.await(), badPeople.await())
    }
    showPeoplesInfo(result.first, result.second)
}
1
启动两个 async
goodPeople 和 badPeople 在 IO 线程并行执行
2
await 等待
两个 await 挂起等待结果,全部到齐后组合 Pair
3
coroutineScope 返回
所有子协程已结束,安全返回 Pair 给外层
4
展示结果
主线程更新 UI

对比两种写法的差异:

// 写法 A:裸 async —— 不符合结构化并发
val deferred = lifecycleScope.async(Dispatchers.IO) {
    apiService.getPeople("yxl")    // 游离在外部 scope
}
lifecycleScope.launch {
    val badPeople = apiService.getPeople("syx")
    val goodPeople = deferred.await()
    showPeoplesInfo(badPeople, goodPeople)
}
// 问题:如果 launch 被取消,外面的 async 仍在运行,浪费资源
// 写法 B:coroutineScope 包裹 —— 符合结构化并发
lifecycleScope.launch {
    val result = coroutineScope {
        val goodPeople = async(Dispatchers.IO) { apiService.getPeople("yxl") }
        val badPeople = async(Dispatchers.IO) { apiService.getPeople("syx") }
        Pair(goodPeople.await(), badPeople.await())
    }
    showPeoplesInfo(result.first, result.second)
}
// 优势:任何一个 async 失败都会取消其他,不会浪费资源

裸 async

  • async 在外部 scope 中游离
  • 取消父协程不会自动取消 async
  • 一个失败,另一个可能还在跑
  • 资源可能浪费

coroutineScope 包裹

  • 所有 async 都在同一个子 scope 中
  • 任一失败即取消其他兄弟协程
  • 全部完成后才返回结果
  • 符合结构化并发原则

五、join 与 coroutineScope:等待初始化完成

6join():等待一个协程执行完毕

有些场景下,你需要在协程中进行初始化操作,完成后再执行后续的一系列方法。join() 就是为此设计的。

lifecycleScope.launch {
    val initJob = launch(Dispatchers.IO) {
        init()       // 耗时初始化操作
    }
    val data = apiService.getData()    // 同时请求数据
    initJob.join()                     // 等待初始化完成
    processData(data)                  // 两者都就绪后处理
}

join() 是一个挂起函数,它会挂起当前协程,直到 initJob 对应的协程执行完毕才会恢复,然后继续执行下面的代码。

1
launch(IO) 启动 init()
2
getData() 同时并行请求
3
join() 挂起,等待 init 完成
4
init 完成,恢复,processData
7coroutineScope 版:更符合结构化并发

上面的 join() 写法虽然能用,但不够"结构化"。用 coroutineScope 可以表达得更清晰:

lifecycleScope.launch {
    val data = coroutineScope {
        launch(Dispatchers.IO) { init() }    // 子协程:初始化
        apiService.getData()                  // 同时获取数据
        // coroutineScope 会自动等待所有子协程完成
        // 最后一个表达式作为返回值
    }
    processData(data)
}

coroutineScope 会自动等待内部所有协程完成,所以不需要显式调用 join()。而且它保证:

  • 如果 init() 失败,getData() 也会被取消(避免资源浪费)
  • 外部调用者拿到 data 时,所有子协程都已经结束了
  • 不会出现"初始化还在跑,已经开始处理数据"的竞态

join() 方式

  • 需要手动管理 initJob 变量
  • init 失败不会取消 getData
  • 不够"结构化"

coroutineScope 方式

  • 自动等待所有子协程
  • 任一失败自动取消其他
  • 代码意图更清晰

join() 仍然有用武之地 —— 当你确实只需要等待某一个特定协程,而不想等待 scope 中其他协程时。但在大多数"等待初始化后执行"的场景下,coroutineScope 是更好的选择

六、核心思想总结

核心要点

一句话总结

launch 并行不等待,suspend 串行先后有序,async/await 组合二者取结果,coroutineScope 把一切收纳进结构化并发的安全边界。