【Kotlin 协程修仙录 · 金丹境 · 初阶】 | 并发艺术:async/await 与并发组合的优雅之道

前言

你已经打通了筑基境的全部经脉。你掌握了结构化并发的根本大法,看透了 CoroutineContext 的内部构造,驯服了 Dispatchers 这匹烈马。你写的协程代码,生命周期安全、线程切换自如、异常处理得当。

但现在,你面临一个更复杂的场景:

商品详情页需要同时加载商品信息用户评价推荐列表。你希望它们并发执行以缩短总耗时------如果串行执行,总耗时是三个请求之和;如果并发执行,总耗时只取决于最慢的那个请求。

你用 launch 分别启动了三个协程,但问题来了:你怎么知道它们都执行完了?你怎么拿到各自的返回值?你怎么保证任何一个失败时正确地处理异常?

你可能会想:用三个 Boolean 标志位,配合 Job.join(),再搞一个 ArrayList 收集结果......写着写着你发现,代码开始向回调地狱的方向滑坡。

协程给出的优雅答案,是两个看似简单却威力巨大的武器:asyncawait

本讲是金丹境的初阶修炼。你将:

  • 彻底搞懂 async/awaitlaunch/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() 时重新抛出
适用场景 不关心结果的"发后即忘"任务 需要返回值的并发计算
flowchart LR subgraph Launch[launch 发射后不管] L1[launch] --> L2[Job] L2 --> L3[join 等待完成] L3 --> L4[无返回值] end subgraph Async[async 需要返回值] A1[async] --> A2[Deferred] A2 --> A3[await 等待结果] A3 --> A4[返回 T] end style Launch fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style Async fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style L1 fill:#90caf9 style L2 fill:#90caf9 style L3 fill:#90caf9 style L4 fill:#90caf9 style A1 fill:#a5d6a7 style A2 fill:#a5d6a7 style A3 fill:#a5d6a7 style A4 fill:#81c784

为什么需要 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!!) // 危险!
}

这段代码有两个致命问题:

  1. 你需要手动声明可变变量(var)来收集结果,破坏了不可变性。
  2. 你无法确定 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 的核心价值:让你能够安全地并发执行多个任务,并优雅地组合它们的结果。

sequenceDiagram participant Caller as 调用方协程 participant Scope as coroutineScope participant A1 as async1 participant A2 as async2 participant A3 as async3 Caller->>Scope: 进入 coroutineScope Scope->>A1: 启动 async1 Scope->>A2: 启动 async2 Scope->>A3: 启动 async3 par 并发执行 A1->>A1: fetchData1() and A2->>A2: fetchData2() and A3->>A3: fetchData3() end Caller->>A1: await() 挂起等待 A1-->>Caller: 返回 data1 Caller->>A2: await() 挂起等待 A2-->>Caller: 返回 data2 Caller->>A3: await() 挂起等待 A3-->>Caller: 返回 data3 Caller->>Scope: 返回组合结果

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 可以省去无谓的资源消耗。

四种模式对比图

stateDiagram-v2 [*] --> Created : 协程创建 Created --> DEFAULT_MODE : start = DEFAULT Created --> LAZY_MODE : start = LAZY Created --> ATOMIC_MODE : start = ATOMIC Created --> UNDISPATCHED_MODE : start = UNDISPATCHED DEFAULT_MODE --> Schedule : 立即提交调度器 LAZY_MODE --> WaitStart : 等待 start/join/await WaitStart --> Schedule : 手动触发后提交 ATOMIC_MODE --> AtomicExec : 立即调度,首个挂起点前不可取消 UNDISPATCHED_MODE --> SyncExec : 当前线程同步执行 Schedule --> NormalExec : 进入正常执行 SyncExec --> AtomicExec : 执行初始代码 AtomicExec --> FirstSuspend : 到达第一个挂起点 FirstSuspend --> NormalExec : 恢复正常取消检查 NormalExec --> Completed : 执行完成 Completed --> [*]

async 的异常传播机制

asynclaunch 在异常处理上有根本性的不同

构建器 异常传播方式 如何处理
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()
flowchart TD subgraph Launch异常["launch 异常传播"] L1[launch 抛出异常] --> L2{有 CoroutineExceptionHandler?} L2 -->|是| L3[Handler 处理] L2 -->|否| L4[崩溃] end subgraph Async异常["async 异常传播"] A1[async 抛出异常] --> A2[异常存入 Deferred] A2 --> A3[调用 await] A3 --> A4[异常重新抛出] A4 --> A5[try-catch 捕获] end style Launch异常 fill:#ffcdd2,stroke:#b71c1c,stroke-width:2px style Async异常 fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px style L4 fill:#e57373 style L3 fill:#ffb74d style A5 fill:#a5d6a7

结构化并发与 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(这将在金丹境·后阶深入讲解)。

sequenceDiagram participant Scope as coroutineScope participant A1 as async1 participant A2 as async2 participant A3 as async3 Scope->>A1: 启动 Scope->>A2: 启动 Scope->>A3: 启动 A2-->>A2: 抛出异常! A2-->>Scope: 异常向上传播 Scope->>A1: 取消信号 Scope->>A3: 取消信号 A1-->>Scope: CancellationException A3-->>Scope: CancellationException Scope->>Scope: 重新抛出 A2 的异常

实战:商品详情页的并发数据加载

让我们用一个完整的 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 生命周期约束
    }
}

始终使用 viewModelScopelifecycleScope 来启动协程。


最佳实践

  1. coroutineScope + async 实现结构化并发组合:这是最安全的并发模式,自动处理异常和取消。

  2. 需要条件性执行时使用 CoroutineStart.LAZY:避免不必要的计算开销。

  3. await() 调用处用 try-catch 处理异常 :这是 async 异常处理的标准姿势。

  4. 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()
  5. 理解 asynclaunch 的异常传播差异:根据是否需要返回值来选择。


总结与下回预告

恭喜,你已经掌握了 async/await 的并发艺术,金丹初成!

本讲核心收获

  • async 返回 Deferred<T>,通过 await() 获取结果;launch 返回 Job,无返回值。
  • CoroutineStart.LAZY 让你可以条件性地启动协程,避免不必要的计算。
  • async 的异常在 await() 时重新抛出,可以用 try-catch 捕获。
  • coroutineScope + async 是结构化并发的黄金组合,任一失败自动取消其他。

在下一讲 【金丹境·中阶】 中,我们将深入 CoroutineStart 的另外三种模式:ATOMICUNDISPATCHED,以及它们的底层实现原理。届时你会明白:

  • ATOMIC 是如何在启动阶段保证不可取消的?
  • UNDISPATCHED 为什么能在当前线程立即执行,直到第一个挂起点?
  • 这些模式在什么极端场景下才会用到?

【当前境界修为面板】

当前境界 修炼技能 修炼进度 修炼心得
金丹境 · 初阶 1、async/await并发组合 2、Deferred结果等待 3、CoroutineStart.LAZY 4、结构化并发与async 当前进度 :25% 修为 :250/1000 下一突破[金丹境 · 中阶] (需领悟:CoroutineStart.ATOMICUNDISPATCHED 的底层原理) launch发射后不管, async发射后还用await回收。 数据结构化的并发,从Deferred开始。

【本讲思考题】

  1. 表象题:以下代码的输出顺序是什么?

    kotlin 复制代码
    suspend fun test() = coroutineScope {
        val d1 = async { delay(1000); println("A") }
        val d2 = async { delay(500); println("B") }
        d1.await()
        d2.await()
        println("C")
    }
  2. 场景题 :你需要在 ViewModel 中并发加载 10 个接口的数据。但其中有一个接口非常慢(可能 10 秒),你希望如果它超过 3 秒还没返回,就放弃它,只展示其他 9 个接口的数据。如何用 asyncwithTimeoutOrNull 实现?

  3. 原理题Deferred 接口继承自 Job。这意味着你可以对一个 async 返回的对象调用 cancel()。如果一个 async 协程被取消后,再调用 await() 会发生什么?请查阅文档并简述。


道友,金丹初成,你已经能用 async 优雅地驾驭多任务并发了。下一讲,我们将深入 CoroutineStart 的底层,看透协程启动的每一个细节。金丹境·中阶见。

欢迎一键四连关注 + 点赞 + 收藏 + 评论

相关推荐
沐言人生2 小时前
ReactNative 源码分析3——ReactActivity之初始化RN应用
android·react native
YaBingSec3 小时前
网络安全靶场WP:Grafana 任意文件读取漏洞(CVE-2021-43798)
android·笔记·安全·web安全·ssh·grafana
YF02113 小时前
彻底解决Android非SDK接口绕过限制的深度实践
android·google·app
IVEN_3 小时前
Gradle 依赖下载 403 Forbidden 修复:全局镜像配置实战
android·后端
恋猫de小郭4 小时前
Flutter 3.44 发布前夕,官方宣布 SwiftPM 将完全取代 CocoaPods
android·前端·flutter
黄林晴4 小时前
重磅发布!KMP 双端订阅支付彻底封神,一套代码搞定 iOS+Android
android·kotlin
Carson带你学Android4 小时前
别再乱学了!深度解读 Google 官方发布 Android 6 大核心 Skills
android·前端·ai编程
张风捷特烈4 小时前
状态管理大乱斗#06 | Riverpod 源码评析 (下) - 外功心法
android·前端·flutter
三少爷的鞋5 小时前
Kotlin 协程 vs Java 虚拟线程:两种并发模型的对比
android