
前言
你已经打通了筑基境的全部经脉。你掌握了结构化并发的根本大法,看透了 CoroutineContext 的内部构造,驯服了 Dispatchers 这匹烈马。你写的协程代码,生命周期安全、线程切换自如、异常处理得当。
但现在,你面临一个更复杂的场景:
商品详情页需要同时加载商品信息 、用户评价 、推荐列表。你希望它们并发执行以缩短总耗时------如果串行执行,总耗时是三个请求之和;如果并发执行,总耗时只取决于最慢的那个请求。
你用 launch 分别启动了三个协程,但问题来了:你怎么知道它们都执行完了?你怎么拿到各自的返回值?你怎么保证任何一个失败时正确地处理异常?
你可能会想:用三个 Boolean 标志位,配合 Job.join(),再搞一个 ArrayList 收集结果......写着写着你发现,代码开始向回调地狱的方向滑坡。
协程给出的优雅答案,是两个看似简单却威力巨大的武器:async 和 await。
本讲是金丹境的初阶修炼。你将:
- 彻底搞懂
async/await与launch/join的本质区别。 - 掌握
async的四种启动模式,特别是LAZY的妙用。 - 学会用
coroutineScope+async实现结构化并发组合。 - 理解
async的异常传播机制,以及它和launch在异常处理上的根本不同。
准备好凝结金丹,驾驭并发了吗?我们开始。
操千曲 而后晓声,观千剑 而后识器。虐它千百遍 方能通晓其真意。
什么是 async?它与 launch 有何本质区别?
在 Kotlin 协程的官方定义中:
async是一个协程构建器,它创建一个新的协程并返回一个Deferred<T>对象。Deferred是一个轻量级的、非阻塞的 Future ,代表一个将在未来某个时刻产生结果的异步计算。你可以通过调用Deferred.await()来挂起当前协程,等待结果返回。
如果说 launch 是"发射后不管"的火箭,那么 async 就是"发射后还要回收返回舱"的航天飞机。
| 对比维度 | launch |
async |
|---|---|---|
| 返回值 | Job |
Deferred<T>(继承自 Job) |
| 是否产生结果 | 否 | 是 |
| 等待方式 | join() 等待完成,不返回结果 |
await() 等待完成,并返回结果 |
| 异常传播 | 未捕获异常立即抛出,或交给 CoroutineExceptionHandler |
未捕获异常在 await() 时重新抛出 |
| 适用场景 | 不关心结果的"发后即忘"任务 | 需要返回值的并发计算 |
为什么需要 async?并发组合的价值
假设你需要调用三个接口,然后将结果组合展示。用 launch 实现的代码可能是这样的:
kotlin
// ❌ 用 launch 实现并发组合的糟糕尝试
suspend fun loadDataWithLaunch(): Triple<Data1, Data2, Data3> {
var data1: Data1? = null
var data2: Data2? = null
var data3: Data3? = null
coroutineScope {
launch { data1 = fetchData1() }
launch { data2 = fetchData2() }
launch { data3 = fetchData3() }
}
// 这里能保证三个 launch 都完成了吗?不能!coroutineScope 只等待它的子协程
// 但 launch 内部的赋值是异步的,这里可能拿到 null
return Triple(data1!!, data2!!, data3!!) // 危险!
}
这段代码有两个致命问题:
- 你需要手动声明可变变量(
var)来收集结果,破坏了不可变性。 - 你无法确定
coroutineScope返回时三个launch内部的赋值是否已经完成。
async 优雅地解决了这两个问题:
kotlin
// ✅ 用 async 实现的优雅并发组合
suspend fun loadDataWithAsync(): Triple<Data1, Data2, Data3> = coroutineScope {
val deferred1 = async { fetchData1() }
val deferred2 = async { fetchData2() }
val deferred3 = async { fetchData3() }
// await() 会挂起当前协程,等待对应 async 完成并返回结果
Triple(deferred1.await(), deferred2.await(), deferred3.await())
}
代码简洁、安全、不可变。这就是 async 的核心价值:让你能够安全地并发执行多个任务,并优雅地组合它们的结果。
async 的四种启动模式
async 有一个可选参数 start: CoroutineStart,它决定了协程的启动时机。默认值是 CoroutineStart.DEFAULT,但还有三种模式值得掌握。
| 启动模式 | 行为 | 适用场景 |
|---|---|---|
DEFAULT |
立即启动,调度器决定何时执行 | 绝大多数场景,默认值 |
LAZY |
不立即启动 ,仅在 await() 或 start() 被调用时才启动 |
需要条件性执行,或计算开销大、不确定是否会用到结果 |
ATOMIC |
立即启动,但在第一个挂起点之前不可取消 | 需要在启动阶段保证原子性(极少用) |
UNDISPATCHED |
立即在当前线程执行,直到第一个挂起点 | 需要减少调度开销,或需要特定线程上下文(极少用) |
LAZY 模式的妙用
kotlin
suspend fun loadUserData(shouldLoadAvatar: Boolean): UserData = coroutineScope {
val userDeferred = async { fetchUser() }
// avatar 的 async 是 LAZY 的,此时还没有开始执行
val avatarDeferred = async(start = CoroutineStart.LAZY) {
fetchAvatar()
}
val user = userDeferred.await()
// 根据条件决定是否真的需要加载头像
val avatar = if (shouldLoadAvatar) {
avatarDeferred.await() // 这里才会真正启动并等待
} else {
null // 如果不需要,async 里的代码根本不会执行
}
UserData(user, avatar)
}
LAZY 的核心价值 :避免不必要的计算。如果你不确定是否会用到某个结果,用 LAZY 可以省去无谓的资源消耗。
四种模式对比图
async 的异常传播机制
async 和 launch 在异常处理上有根本性的不同。
| 构建器 | 异常传播方式 | 如何处理 |
|---|---|---|
launch |
异常立即抛出 ,或交给 CoroutineExceptionHandler |
在根协程设置 CoroutineExceptionHandler |
async |
异常被包装在 Deferred 中 ,在调用 await() 时重新抛出 |
在 await() 调用处用 try-catch 包裹 |
kotlin
fun main() = runBlocking {
// launch:异常立即传播
val job = launch {
throw RuntimeException("launch 中的异常")
}
// 程序可能在这里就崩溃了(如果没有 handler)
// async:异常在 await 时抛出
val deferred = async {
throw RuntimeException("async 中的异常")
}
try {
deferred.await() // 异常在这里抛出
} catch (e: Exception) {
println("捕获到:${e.message}")
}
}
核心结论:
- 如果你需要"发后即忘"且不关心异常,用
launch+CoroutineExceptionHandler。 - 如果你需要拿到结果并自己处理异常,用
async+try-catch await()。
结构化并发与 async 的组合:coroutineScope 内的 async
async 最优雅的用法是与 coroutineScope 结合。这种组合产生了一个非常强大的特性:任何一个 async 失败,coroutineScope 会自动取消所有其他还在执行的 async,并抛出异常。
这正是结构化并发的精髓------一个失败,全体遭殃,避免资源浪费。
kotlin
suspend fun loadAllData(): CombinedData = coroutineScope {
val deferred1 = async { fetchData1() }
val deferred2 = async { fetchData2() }
val deferred3 = async { fetchData3() }
// 如果任何一个 await 抛出异常,coroutineScope 会:
// 1. 立即取消 deferred1、deferred2、deferred3 中还在执行的
// 2. 将该异常重新抛出
CombinedData(
deferred1.await(),
deferred2.await(),
deferred3.await()
)
}
如果你希望某个 async 的失败不影响其他兄弟(例如推荐列表失败,商品信息仍然显示),你应该使用 supervisorScope(这将在金丹境·后阶深入讲解)。
实战:商品详情页的并发数据加载
让我们用一个完整的 Android 实战案例来巩固 async 的用法。场景是商品详情页,需要并发加载三项数据,并优雅处理加载状态和异常。
kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
class ProductDetailViewModel(
private val productRepo: ProductRepository,
private val reviewRepo: ReviewRepository,
private val recommendRepo: RecommendRepository
) : ViewModel() {
sealed class UiState {
object Loading : UiState()
data class Success(
val product: Product,
val reviews: List<Review>,
val recommends: List<Product>
) : UiState()
data class Error(val message: String) : UiState()
}
var uiState by mutableStateOf<UiState>(UiState.Loading)
private set
fun loadProductDetails(productId: String) {
viewModelScope.launch {
uiState = UiState.Loading
uiState = try {
// coroutineScope 保证并发执行 + 异常自动取消
val result = coroutineScope {
val productDeferred = async { productRepo.getProduct(productId) }
val reviewsDeferred = async { reviewRepo.getReviews(productId) }
val recommendsDeferred = async {
// 推荐接口可能较慢,使用 LAZY 避免不必要的等待?
// 这里不需要 LAZY,因为我们确实需要它的结果
recommendRepo.getRecommends(productId)
}
UiState.Success(
product = productDeferred.await(),
reviews = reviewsDeferred.await(),
recommends = recommendsDeferred.await()
)
}
result
} catch (e: Exception) {
UiState.Error("加载失败:${e.message}")
}
}
}
}
配合 Compose UI:
kotlin
@Composable
fun ProductDetailScreen(viewModel: ProductDetailViewModel = viewModel()) {
when (val state = viewModel.uiState) {
is ProductDetailViewModel.UiState.Loading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
is ProductDetailViewModel.UiState.Success -> {
Column {
Text("商品:${state.product.name}")
Text("评价数:${state.reviews.size}")
Text("推荐:${state.recommends.joinToString { it.name }}")
}
}
is ProductDetailViewModel.UiState.Error -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("出错了:${state.message}", color = Color.Red)
}
}
}
}
常见错误与避坑指南
错误 1:忘记调用 await(),以为 async 已经执行
kotlin
// ❌ 错误:没有调用 await(),协程可能还没执行完
suspend fun loadData(): Data = coroutineScope {
val deferred = async { fetchData() }
// 没有 await()!coroutineScope 返回时 deferred 可能还没完成
Data() // 返回了空数据
}
正确做法 :必须调用 deferred.await() 来等待结果。
错误 2:在 async 块外捕获异常,以为能保护 await
kotlin
// ❌ 错误:try-catch 包裹了整个 async,但异常在 await 时抛出
try {
val deferred = async { throw RuntimeException() }
val result = deferred.await() // 异常在这里抛出,没有被 catch
} catch (e: Exception) {
// 实际上这里能捕获!因为 await 在 try 块内
}
这段代码其实是正确的 。常见的误解是以为异常在 async 块内就被抛出了。记住:异常在 await() 时抛出 ,所以 try-catch 包裹 await() 是正确的。
错误 3:在 LAZY 模式下多次调用 await
kotlin
val deferred = async(start = CoroutineStart.LAZY) { fetchData() }
val result1 = deferred.await() // 第一次:启动并等待
val result2 = deferred.await() // 第二次:立即返回缓存结果(没问题)
LAZY 的 Deferred 在第一次 await 后结果会被缓存,后续 await 立即返回。这没有问题,但要知道这个行为。
错误 4:用 GlobalScope.async 替代 viewModelScope.async
kotlin
// ❌ 危险:生命周期不受控制
fun loadData() {
GlobalScope.async {
// 这个协程不受 ViewModel 生命周期约束
}
}
始终使用 viewModelScope 或 lifecycleScope 来启动协程。
最佳实践
-
用
coroutineScope+async实现结构化并发组合:这是最安全的并发模式,自动处理异常和取消。 -
需要条件性执行时使用
CoroutineStart.LAZY:避免不必要的计算开销。 -
await()调用处用try-catch处理异常 :这是async异常处理的标准姿势。 -
将
Deferred的声明和await分开 :先声明所有Deferred(让它们并发启动),再逐个await。如果声明一个就await一个,就变成串行了。kotlin// ❌ 串行执行 val r1 = async { f1() }.await() val r2 = async { f2() }.await() // ✅ 并发执行 val d1 = async { f1() } val d2 = async { f2() } val r1 = d1.await() val r2 = d2.await() -
理解
async和launch的异常传播差异:根据是否需要返回值来选择。
总结与下回预告
恭喜,你已经掌握了 async/await 的并发艺术,金丹初成!
本讲核心收获:
async返回Deferred<T>,通过await()获取结果;launch返回Job,无返回值。CoroutineStart.LAZY让你可以条件性地启动协程,避免不必要的计算。async的异常在await()时重新抛出,可以用try-catch捕获。coroutineScope+async是结构化并发的黄金组合,任一失败自动取消其他。
在下一讲 【金丹境·中阶】 中,我们将深入 CoroutineStart 的另外三种模式:ATOMIC、UNDISPATCHED,以及它们的底层实现原理。届时你会明白:
ATOMIC是如何在启动阶段保证不可取消的?UNDISPATCHED为什么能在当前线程立即执行,直到第一个挂起点?- 这些模式在什么极端场景下才会用到?
【当前境界修为面板】
| 当前境界 | 修炼技能 | 修炼进度 | 修炼心得 |
|---|---|---|---|
| 金丹境 · 初阶 | 1、async/await并发组合 2、Deferred结果等待 3、CoroutineStart.LAZY 4、结构化并发与async |
当前进度 :25% 修为 :250/1000 下一突破 : [金丹境 · 中阶] (需领悟:CoroutineStart.ATOMIC、 UNDISPATCHED 的底层原理) |
launch发射后不管, async发射后还用await回收。 数据结构化的并发,从Deferred开始。 |
【本讲思考题】
-
表象题:以下代码的输出顺序是什么?
kotlinsuspend fun test() = coroutineScope { val d1 = async { delay(1000); println("A") } val d2 = async { delay(500); println("B") } d1.await() d2.await() println("C") } -
场景题 :你需要在 ViewModel 中并发加载 10 个接口的数据。但其中有一个接口非常慢(可能 10 秒),你希望如果它超过 3 秒还没返回,就放弃它,只展示其他 9 个接口的数据。如何用
async和withTimeoutOrNull实现? -
原理题 :
Deferred接口继承自Job。这意味着你可以对一个async返回的对象调用cancel()。如果一个async协程被取消后,再调用await()会发生什么?请查阅文档并简述。
道友,金丹初成,你已经能用 async 优雅地驾驭多任务并发了。下一讲,我们将深入 CoroutineStart 的底层,看透协程启动的每一个细节。金丹境·中阶见。
欢迎一键四连 (
关注+点赞+收藏+评论)