ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Kotlin协程中launch与async的核心区别与实战应用

Kotlin协程中launch与async的核心区别与实战应用 在 Android 开发中尤其是使用 Kotlin 协程时launch和async是两个最常用但也最容易混淆的构建器。很多开发者包括我自己在项目初期都曾困惑于它们的选择为什么有时候用launch会丢失结果而用async又感觉多此一举这种困惑往往源于对两者核心职责和返回机制的理解不够深入。本文将彻底厘清launch和async的区别从设计目的、使用场景、异常处理到实战中的最佳实践提供一个从入门到精通的完整指南。无论你是刚接触协程的新手还是希望优化现有代码结构的进阶开发者都能从中找到清晰的答案和可直接复用的代码示例。1. 协程构建器launch与async的核心定位在深入区别之前我们必须理解 Kotlin 协程的一个基本概念协程构建器Coroutine Builder。它们是用来启动一个新协程的函数并且必须在协程作用域CoroutineScope中调用。launch和async就是 Kotlin 标准库中最主要的两个构建器。1.1launch启动一个“独立任务”你可以把launch想象成“发射一枚火箭”。你下达发射指令后火箭新协程就独立地去执行它的任务一段耗时代码而你调用方不会立即得到这个任务的结果。launch的设计初衷是执行一段不需要返回结果的、可能耗时的后台任务。它的核心特点是返回一个Job对象Job代表了这个协程任务本身你可以用它来取消任务job.cancel()或等待任务完成job.join()但你无法通过Job直接获取任务内部的计算结果。“即发即弃”调用launch后当前代码会立即继续向下执行不会阻塞。适用于副作用操作比如更新 UI在 Android 主线程上、写入日志、发送网络请求不关心返回数据只关心成功/失败状态等。1.2async启动一个“有返回值的计算任务”相比之下async更像是“派出一支侦察队并要求带回情报”。你启动一个任务并且明确期望它最终会给你带回一个结果。async用于启动一个并发的、需要返回计算结果的任务。它的核心特点是返回一个DeferredT对象Deferred是Job的子接口它代表了一个在未来某个时刻才会产生结果类型为T的承诺Promise。你可以通过调用Deferred的await()方法来挂起当前协程并等待其内部任务完成并获取结果。用于并发计算这是async最强大的能力。你可以同时启动多个async任务它们会并发执行最后再通过await()收集所有结果从而显著提升整体执行效率。必须等待结果虽然启动async不会阻塞但如果你需要它的结果就必须在某个时刻调用await()这会挂起当前协程直到结果就绪。简单记忆launch用于“做事”async用于“取数”。2. 环境准备与项目配置为了运行本文的所有示例你需要一个支持 Kotlin 协程的环境。在 Android 项目中配置非常简单。2.1 依赖配置在你的app模块的build.gradle.kts(或build.gradle) 文件中确保添加了协程库依赖。通常使用最新稳定版。// build.gradle.kts (Module: app) dependencies { // Kotlin协程核心库 implementation(org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0) // Android主线程调度器在Android项目中必需 implementation(org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0) // 如果你在纯Kotlin/JVM项目非Android中测试则使用以下依赖 // implementation(org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0) }2.2 创建测试环境你可以直接在 Android 的ViewModel、Activity或一个简单的 Kotlin JVM 项目中测试。为了方便演示我们创建一个简单的 Kotlinmain函数作为 playground。在 Android Studio 中可以创建一个新的 Kotlin 文件例如CoroutineDemo.kt。// 文件CoroutineDemo.kt import kotlinx.coroutines.* import kotlin.system.measureTimeMillis fun main() runBlocking { // runBlocking 创建一个阻塞当前线程的协程作用域用于测试 println(主协程开始线程${Thread.currentThread().name}) // 在这里编写你的 launch/async 测试代码 // 示例1: launchDemo() // 示例2: asyncDemo() println(主协程结束) } // 我们后续的示例函数将在这里定义 suspend fun launchDemo() { ... } suspend fun asyncDemo() { ... }runBlocking会阻塞其所在的线程这里是主线程直到其内部的所有子协程都执行完毕。它非常适合在main函数或测试中用来运行挂起函数。3. 核心区别深度解析与代码示例让我们通过具体的代码来感受两者的不同。假设我们有一个模拟的耗时任务获取用户名称。suspend fun fetchUserName(): String { delay(1000) // 模拟1秒网络请求 return “CSDN_User” }3.1launch的基本用法与局限场景我们只需要在后台获取用户名然后打印它或者用它来更新 UI但调用方不直接需要这个字符串结果。suspend fun launchDemo() { println(“开始 launchDemo”) val job launch { // 启动一个新协程返回 Job val name fetchUserName() println(“在 launch 协程中获取到用户名$name”) // 副作用打印 // 这里可以更新UI: userNameTextView.text name } println(“launch 调用完毕立即执行这里。Job 是$job”) job.join() // 等待 launch 协程执行完毕可选 println(“launchDemo 结束”) } // 输出顺序 // 开始 launchDemo // launch 调用完毕立即执行这里。Job 是StandaloneCoroutine{Active}... // 等待约1秒 // 在 launch 协程中获取到用户名CSDN_User // launchDemo 结束关键点launch { ... }内部的println是它的“副作用”外部无法直接获取name变量。调用launch后主协程立即继续执行打印“launch 调用完毕...”。job.join()是一个挂起函数它会挂起launchDemo协程直到job代表的任务完成。如果不调用join()主协程可能在子协程打印名字前就结束了。3.2async的基本用法与结果获取场景我们需要获取用户名并在主协程中使用这个结果进行后续操作。suspend fun asyncDemo() { println(“开始 asyncDemo”) val deferred: DeferredString async { // 启动一个新协程返回 DeferredString fetchUserName() } println(“async 调用完毕立即执行这里。Deferred 是$deferred”) // 在需要结果的时候调用 await() val result: String deferred.await() // 挂起点等待结果 println(“获取到 async 的结果$result”) println(“asyncDemo 结束”) } // 输出顺序 // 开始 asyncDemo // async 调用完毕立即执行这里。Deferred 是DeferredCoroutine{Active}... // 等待约1秒 // 获取到 async 的结果CSDN_User // asyncDemo 结束关键点async { ... }的最后一个表达式fetchUserName()的结果String类型就是DeferredString所承载的最终结果。deferred.await()是核心。它会挂起当前协程直到async任务完成然后返回结果。没有await()async任务虽然会执行但结果会被忽略。3.3 核心区别对比表格特性launchasync返回类型JobDeferredT(继承自Job)设计目的执行不返回结果的副作用操作如更新UI、日志、触发事件。执行需要返回计算结果的并发任务。结果获取无法直接获取。结果需通过回调、Channel、共享状态等方式传递。通过调用返回的Deferred.await()挂起并获取结果T。异常处理未捕获的异常会立即传播到父协程/作用域可能导致整个作用域取消。异常会延迟到调用await()时才抛出给了调用方处理异常的时机。典型用例后台任务、生命周期管理、事件触发。并发网络请求、并行计算、聚合多个数据源。启动行为“即发即弃”不阻塞。“启动并等待”await()会阻塞当前协程。4. 实战案例并发网络请求与结构化并发async真正的威力在于并发。假设我们需要从两个独立的 API 获取用户基本信息和订单列表然后合并展示。4.1 错误示范顺序执行suspend fun fetchUserInfo(): String { delay(1000); return “{name: ‘Alice’, age: 30}” } suspend fun fetchUserOrders(): String { delay(1200); return “[order1, order2]” } suspend fun sequentialFetch() { val time measureTimeMillis { val info fetchUserInfo() // 挂起1秒 val orders fetchUserOrders() // 在前一个完成后再挂起1.2秒 println(“用户信息$info 订单$orders”) } println(“顺序执行耗时${time}ms”) // 总耗时约 2200ms }4.2 正确示范使用async并发执行suspend fun concurrentFetch() coroutineScope { // 创建一个新的协程作用域 val time measureTimeMillis { val deferredInfo async { fetchUserInfo() } // 立即启动并发执行 val deferredOrders async { fetchUserOrders() } // 立即启动并发执行 // 两个任务已经在并发运行这里同时等待它们的结果 val info deferredInfo.await() // 等待第一个结果可能已就绪 val orders deferredOrders.await() // 等待第二个结果 println(“用户信息$info 订单$orders”) } println(“并发执行耗时${time}ms”) // 总耗时约 1200ms (取决于最慢的任务) }关键改进coroutineScope这是一个挂起函数它会创建一个新的协程作用域。这个作用域有一个重要特性结构化并发。它保证在其内部启动的所有子协程async完成之前coroutineScope自身不会结束。同时如果其中一个子协程失败所有其他兄弟协程也会被取消。并发启动两个async几乎是同时启动的fetchUserInfo和fetchUserOrders并行执行。并行等待await()的调用顺序不影响总耗时。即使先await订单再await信息总时间依然是两个任务中较长的那个约1.2秒而不是它们的和。性能提升从 ~2200ms 降低到 ~1200ms效率几乎翻倍。4.3 结合launch与async的复杂场景考虑一个更实际的场景在 Android 上我们并发获取数据async然后在主线程上用这些数据更新 UIlaunch配合Dispatchers.Main。// 这是一个在 Android ViewModel 中的示例 class UserViewModel : ViewModel() { private val _userData MutableStateFlow(“加载中...”) val userData: StateFlowString _userData.asStateFlow() fun loadUserData() { viewModelScope.launch { // 在 ViewModel 的作用域中启动一个父协程 // 1. 在IO线程并发获取数据 val deferredInfo async(Dispatchers.IO) { repository.fetchUserInfo() } val deferredOrders async(Dispatchers.IO) { repository.fetchUserOrders() } // 2. 等待数据挂起但不阻塞主线程 val info deferredInfo.await() val orders deferredOrders.await() // 3. 回到主线程更新UIStateFlow _userData.value “信息$info, 订单$orders” // 或者直接使用 launch(Dispatchers.Main) { ... } 来更新UI控件 } // viewModelScope 会管理这个协程的生命周期当 ViewModel 被清除时自动取消 } }在这个模式中viewModelScope.launch是主协程它管理整个数据加载流程。内部的async(Dispatchers.IO)负责在后台线程池执行耗时 IO 操作。await()挂起主协程等待后台任务完成。最后在主协程中默认在viewModelScope的上下文中通常是主线程安全地更新StateFlow或 UI 状态。5. 异常处理机制的关键差异这是launch和async另一个至关重要的区别处理不当会导致应用崩溃。5.1launch的异常传播自动取消与崩溃suspend fun launchExceptionDemo() coroutineScope { val job launch { println(“子协程开始”) delay(500) throw RuntimeException(“launch 内部出错”) // 未捕获的异常 println(“这行不会执行”) } delay(1000) // 等待一段时间观察异常影响 println(“父作用域还活着吗”) // 这行可能不会执行 } // 输出 // 子协程开始 // 程序可能崩溃输出异常栈轨迹发生了什么在launch中抛出的未捕获异常会立即传播到它的父协程即coroutineScope。默认情况下这会取消父作用域及其所有子协程并且如果异常到达了根协程且未被CoroutineExceptionHandler处理可能会导致程序崩溃。5.2async的异常传播延迟抛出suspend fun asyncExceptionDemo() coroutineScope { val deferred async { println(“async 任务开始”) delay(500) throw RuntimeException(“async 内部出错”) “结果” } delay(1000) // 注意即使 async 内部出错了这里依然会等待1秒 println(“准备获取结果...”) try { val result deferred.await() // 异常在这里才被抛出 println(“结果$result”) } catch (e: Exception) { println(“捕获到 async 的异常${e.message}”) } println(“父作用域正常结束。”) } // 输出 // async 任务开始 // 准备获取结果... // 捕获到 async 的异常async 内部出错 // 父作用域正常结束。关键区别延迟抛出async内部的异常会被封装在Deferred对象里不会立即抛出。父作用域不会被立即取消。调用时抛出只有当你对这个Deferred调用.await()时存储的异常才会被重新抛出。这给了调用方一个集中处理错误的机会。结构化并发依然有效如果async是coroutineScope的子协程并且它在await()之前就失败了当coroutineScope结束时它仍然会感知到失败并取消其他兄弟协程。但异常传播的时机被推迟了。5.3 异常处理最佳实践对于launch总是使用try/catch包裹可能出错的代码块或者为协程作用域设置CoroutineExceptionHandler。val handler CoroutineExceptionHandler { _, exception - println(“Caught $exception”) } val scope CoroutineScope(Job() handler) scope.launch { // 可能出错的代码 }对于async在调用await()的地方使用try/catch进行包裹。如果启动了多个async可以考虑将Deferred对象收集起来然后统一处理它们的异常。val deferreds listOf(async { task1() }, async { task2() }) deferreds.forEach { deferred - try { deferred.await() } catch (e: Exception) { // 处理单个任务失败 } }supervisorScope当你希望一个子协程的失败不影响其他兄弟协程和父作用域时使用supervisorScope代替coroutineScope。这在 UI 组件中很常见比如一个页面中多个独立的网络请求一个失败不应导致整个页面数据加载取消。6. 常见问题与排查思路在实际使用中你会遇到一些典型问题。下面是一个快速排查指南。问题现象可能原因排查步骤与解决方案调用await()后程序挂起不执行1.async任务内部有死锁或无限循环。2. 任务所在的协程上下文被取消导致async任务被取消。1. 检查async块内的逻辑确保有明确的完成条件。2. 检查父协程的Job是否已被取消。可以使用ensureActive()在任务中检查取消状态。launch启动的任务好像没执行1. 启动launch的作用域生命周期太短在任务执行前就被销毁了如 Activity 被关闭。2. 没有调用join()等待且主协程提前结束。1. 在 Android 中使用viewModelScope或lifecycleScope等与生命周期绑定的作用域。2. 如果需要在测试中等待使用job.join()或runBlocking。async并发没有提速1. 任务不是真正的 IO/CPU 密集型或者都在同一个单线程上下文中运行如Dispatchers.Main。2. 错误地顺序调用了await()。1. 确保为耗时任务指定了合适的调度器如Dispatchers.IO或Dispatchers.Default。2. 确保在启动所有async任务之后再依次调用await()。异常导致应用崩溃1.launch中异常未捕获。2.async中异常未在await()时捕获。3. 根协程没有设置异常处理器。1. 在launch块内使用try/catch。2. 在await()调用处使用try/catch。3. 为顶级协程作用域设置CoroutineExceptionHandler。内存泄漏在Activity/Fragment中使用了全局的CoroutineScope或GlobalScope并且协程捕获了视图引用。1.永远避免在 UI 组件中使用GlobalScope.launch。2. 使用lifecycleScope或viewModelScope它们会在组件销毁时自动取消所有协程。3. 在协程内避免直接引用View使用ViewModel或数据流。7. 最佳实践与工程建议掌握基础后遵循以下实践能让你的协程代码更健壮、更易维护。7.1 明确选择构建器需要结果吗需要 - 用async。不需要 - 用launch。任务是并发的吗多个独立任务可以并行以提高性能 - 用多个async最后统一await。只是触发一个后台事件吗如保存日志、发送分析事件 - 用launch。7.2 始终使用结构化并发避免使用GlobalScope它创建的生命周期不受控的顶级协程是内存泄漏的常见根源。使用限定的作用域在 Android 中viewModelScope、lifecycleScope是你的好朋友。在普通函数中使用coroutineScope或supervisorScope来创建子作用域。作用域负责取消当一个作用域被取消时它内部的所有子协程都会被自动取消这简化了资源清理。7.3 妥善处理取消与异常响应取消在协程内部的循环或长时间计算中定期调用ensureActive()或检查isActive以便在协程被取消时及时退出。区分错误处理使用try/catch处理可恢复的业务异常。使用CoroutineExceptionHandler处理未捕获的、非预期的异常通常用于日志记录。supervisorScope用于独立任务如果一个作用域内的多个子任务是独立的一个失败不应影响其他使用supervisorScope。7.4 调度器选择Dispatchers.Main更新 UI。在 Android 上viewModelScope.launch默认使用此调度器。Dispatchers.IO执行磁盘或网络 I/O 操作。线程池会根据需要扩展。Dispatchers.Default执行 CPU 密集型计算如图像处理、复杂算法。线程数通常与 CPU 核心数相关。Dispatchers.Unconfined一般不用于生产代码主要用于测试或特定高级场景。它不限制协程在特定线程上恢复。7.5 测试协程测试需要特殊处理因为涉及挂起和并发。使用runTestkotlinx-coroutines-test库来编写测试它可以控制虚拟时间使测试变得确定且快速。Test fun test concurrent fetch() runTest { // 使用 runTest val viewModel UserViewModel() viewModel.loadUserData() // 可以通过 advanceTimeBy(…) 控制虚拟时间 advanceTimeBy(1500) // 断言 StateFlow 的值 assertEquals(“信息..., 订单...”, viewModel.userData.value) }理解launch和async的区别是写出高效、健壮 Kotlin 协程代码的基石。launch是你的“任务执行者”专注于完成工作本身而async是你的“数据获取者”专注于并发计算并带回成果。记住async必须配合await()使用并且它的异常是延迟抛出的。在 Android 开发中结合viewModelScope和LifecycleScope遵循结构化并发原则能有效避免内存泄漏和生命周期问题。下次当你需要启动一个协程时先问自己“我需要这个操作的结果吗” 答案会清晰地指引你选择正确的工具。
返回列表