灵活组合"并发执行"与"串行等待",通过调度器控制执行线程,用 coroutineScope 实现真正的结构化并发
协程中最直接的并行方式:用多个 launch 分别启动协程,它们各自运行在指定的调度器上,同时执行、互不干扰。
// 两个 launch 分别运行在 IO 线程,同时执行
lifecycleScope.launch(Dispatchers.IO) {
val people = apiService.getUser("yxl")
// 处理 yxl 的数据...
}
lifecycleScope.launch(Dispatchers.IO) {
val people = apiService.getUser("syx")
// 处理 syx 的数据...
}
这两个协程的关系是完全独立的:
这种方法适合没有依赖关系的独立任务。两个请求的数据互不需要,各自独立处理即可。最典型的场景:首页同时拉取多个不相关的数据源。
当一个任务依赖前一个任务的结果时,需要串行执行。挂起函数天然支持这种模式:写在后面的代码一定在前面挂起函数恢复之后才执行。
// 使用挂起函数,先请求数据再更新界面
lifecycleScope.launch {
val people = apiService.getUser("yxl") // 先获取数据(后台线程)
showPeopleInfo(people) // 再更新界面(主线程)
}
重要认知:挂起函数本身并不自动切换线程。实际线程的切换通常由 apiService 内部通过 withContext(Dispatchers.IO) 完成。挂起只是"暂停",恢复时回到哪个线程取决于调度器的安排。
串行模式的核心优势:后面的代码可以直接使用挂起函数的返回值。后台线程执行的网络请求结果,主线程可以直接拿来刷新界面。
如果一个方法需要整合两个网络请求的数据再执行后续操作,单纯用 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,所以它既是一个协程句柄,也承载返回值。
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 的数据deferred.await() 是挂起函数 —— 如果数据还没回来就挂起等待但这种写法不符合结构化并发。async 协程在外部 scope 中游离,如果 launch 内部失败或被取消,外层的 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)
}
对比两种写法的差异:
// 写法 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 失败都会取消其他,不会浪费资源
有些场景下,你需要在协程中进行初始化操作,完成后再执行后续的一系列方法。join() 就是为此设计的。
lifecycleScope.launch {
val initJob = launch(Dispatchers.IO) {
init() // 耗时初始化操作
}
val data = apiService.getData() // 同时请求数据
initJob.join() // 等待初始化完成
processData(data) // 两者都就绪后处理
}
join() 是一个挂起函数,它会挂起当前协程,直到 initJob 对应的协程执行完毕才会恢复,然后继续执行下面的代码。
上面的 join() 写法虽然能用,但不够"结构化"。用 coroutineScope 可以表达得更清晰:
lifecycleScope.launch {
val data = coroutineScope {
launch(Dispatchers.IO) { init() } // 子协程:初始化
apiService.getData() // 同时获取数据
// coroutineScope 会自动等待所有子协程完成
// 最后一个表达式作为返回值
}
processData(data)
}
coroutineScope 会自动等待内部所有协程完成,所以不需要显式调用 join()。而且它保证:
init() 失败,getData() 也会被取消(避免资源浪费)data 时,所有子协程都已经结束了join() 仍然有用武之地 —— 当你确实只需要等待某一个特定协程,而不想等待 scope 中其他协程时。但在大多数"等待初始化后执行"的场景下,coroutineScope 是更好的选择。
launch 并行不等待,suspend 串行先后有序,async/await 组合二者取结果,coroutineScope 把一切收纳进结构化并发的安全边界。