Kotlin协程launch与async核心区别:从概念到实战全面解析
在 Android 开发中尤其是使用 Kotlin 协程进行异步编程时launch和async是两个最核心、最常用的构建器。很多开发者尤其是刚接触协程的同学常常对它们的使用场景和区别感到困惑为什么有时候用launch有时候又必须用async它们返回的Job和Deferred到底有何不同在结构化并发中它们的行为又有何差异本文将为你彻底厘清launch和async的区别。我们会从最基础的概念讲起通过大量可运行的代码示例对比它们在启动方式、返回值、异常处理、取消行为以及适用场景上的不同。无论你是正在学习协程的新手还是希望深入理解其内部机制的中级开发者这篇文章都能帮你构建清晰的知识体系避免在实际项目中踩坑。1. 协程构建器launch与async的核心概念在深入区别之前我们必须先理解它们在 Kotlin 协程体系中的定位。它们都是协程构建器是创建和启动一个新协程的“入口点”。1.1 什么是协程构建器协程构建器是一个函数它接收一个挂起函数通常是 lambda 表达式作为参数并在特定的协程作用域内启动一个新的协程来执行这个函数。Kotlin 标准库和协程库提供了多个构建器launch和async是其中最常用的两个。你可以把协程作用域想象成一个“管理区域”它定义了协程的生命周期和父子关系。launch和async必须在一个作用域内被调用最常见的就是CoroutineScope的扩展函数。1.2launch启动一个“不关心结果”的协程launch构建器用于启动一个不返回结果的协程。它的设计初衷是执行一段“一劳永逸”的后台任务比如日志记录、数据上报、状态更新等。你启动它然后通常就不需要或无法直接获取它的执行结果了。它的函数签名简化如下fun CoroutineScope.launch( context: CoroutineContext EmptyCoroutineContext, start: CoroutineStart CoroutineStart.DEFAULT, block: suspend CoroutineScope.() - Unit ): Job关键点返回值Job。一个代表协程生命周期的句柄你可以用它来取消协程 (job.cancel())或者等待协程完成 (job.join())。内部逻辑block是一个没有返回值的挂起函数。协程执行完毕后其结果或者说没有结果不会通过launch本身返回。1.3async启动一个“关心结果”的协程async构建器用于启动一个需要返回结果的协程。它的典型场景是并发执行多个独立的任务并等待所有任务完成后再汇总结果也就是我们常说的“并行计算”。它的函数签名简化如下fun T CoroutineScope.async( context: CoroutineContext EmptyCoroutineContext, start: CoroutineStart CoroutineStart.DEFAULT, block: suspend CoroutineScope.() - T ): DeferredT关键点返回值DeferredT。这是一个轻量级的、非阻塞的 future可以理解为“期约”。它继承自Job所以同样具备取消和等待的能力。此外它多了一个await()方法用于获取协程执行的结果T。内部逻辑block是一个有返回值的挂起函数。协程执行的结果会被包装在Deferred对象中。简单来说launch是“发射后不管”而async是“发射后等回音”。这个根本目的的不同衍生出了它们在其他方面的行为差异。2. 环境准备与项目配置为了运行本文的所有示例你需要一个支持 Kotlin 协程的 Android 或 Kotlin/JVM 项目。2.1 依赖配置在你的app模块的build.gradle.kts(或build.gradle) 文件中确保添加了协程库依赖。通常使用最新稳定版。// build.gradle.kts (Module: app) dependencies { // Kotlin Coroutines implementation(org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0) // 请检查最新版本 // 如果是纯 Kotlin/JVM 项目使用 core 即可 // implementation(org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0) }2.2 创建测试环境我们可以创建一个简单的 Kotlin 文件例如CoroutineDemo.kt并在main函数中运行示例。对于 Android 项目你可以在ViewModel、Activity或一个简单的测试函数中运行。为了模拟耗时操作我们定义一个通用的挂起函数import kotlinx.coroutines.* import kotlin.system.measureTimeMillis // 模拟一个耗时任务返回一个字符串结果 suspend fun doWork(name: String, delayMillis: Long): String { delay(delayMillis) // 模拟网络请求或复杂计算 return Result from $name }现在环境已经就绪让我们通过具体的代码来感受launch和async的核心区别。3. 核心区别一返回值与结果获取这是两者最直观、最重要的区别。3.1launch返回Job无法直接获取结果fun main() runBlocking { println(Start of launch example) val job: Job launch { val result doWork(Launch Task, 1000L) println(Work done inside launch: $result) // 结果只能在协程内部使用 } println(launch() returned a Job: $job) job.join() // 等待协程执行完毕 println(End of launch example) // 注意我们无法在 job 对象上获取 doWork 返回的字符串。 }输出Start of launch example launch() returned a Job: StandaloneCoroutine{Active}... Work done inside launch: Result from Launch Task End of launch example如你所见launch返回的job对象只告诉我们协程的状态是否活跃、是否完成、是否取消但无法从中提取doWork函数返回的“Result from Launch Task”。结果被打印在协程内部外部无法直接访问。3.2async返回DeferredT通过await()获取结果fun main() runBlocking { println(Start of async example) val deferred: DeferredString async { doWork(Async Task, 1000L) } println(async() returned a Deferred: $deferred) // 在需要结果的时候调用 await() val result: String deferred.await() println(Result obtained from async: $result) println(End of async example) }输出Start of async example async() returned a Deferred: DeferredCoroutine{Active}... Result obtained from async: Result from Async Task End of async exampleasync返回的deferred对象是一个结果的容器。调用deferred.await()会挂起当前协程直到async协程执行完毕然后返回其结果。这是async存在的根本价值。4. 核心区别二异常处理异常处理是协程中一个容易出错的部分launch和async在异常传播机制上有显著不同。4.1launch的异常立即传播与崩溃默认情况下在launch内部抛出的未捕获异常会立即传播到父协程/作用域可能导致整个作用域取消和崩溃在 Android 上可能导致 App 闪退。fun main() runBlocking { println(Start) val job launch { println(Child launch started) delay(100) throw RuntimeException(Oops! Error in launch!) // 抛出异常 } try { job.join() } catch (e: Exception) { // 注意这里的 catch 块可能抓不到异常 println(Caught exception from join: $e) } delay(500) // 给一点时间观察 println(End - This line may not be printed if the exception propagates.) } // 输出可能因协程版本和上下文而异但通常父协程(runBlocking)会因未捕获异常而终止。关键点launch的异常倾向于“向上冒泡”。为了处理launch内部的异常通常需要使用CoroutineExceptionHandler或者用try/catch包裹协程体内部可能出错的代码。4.2async的异常延迟传播与await()捕获async内部的异常处理则更加“惰性”。异常不会立即抛出而是被封装在Deferred对象中。只有当你调用deferred.await()试图获取结果时异常才会被重新抛出。fun main() runBlocking { println(Start) val deferred async { println(Child async started) delay(100) throw RuntimeException(Oops! Error in async!) This will never be returned } delay(300) // 注意即使 async 内部已经抛出异常这里也不会崩溃 println(Deferred created, exception is encapsulated.) try { val result deferred.await() // 异常在此处被抛出 println(Result: $result) } catch (e: Exception) { println(Caught exception from await(): $e) // 异常在这里被捕获 } println(End - This line will be printed because we caught the exception.) }输出Start Child async started Deferred created, exception is encapsulated. Caught exception from await(): java.lang.RuntimeException: Oops! Error in async! End - This line will be printed because we caught the exception.关键点async的异常被“存储”起来了这给了你更大的灵活性。你可以在一个统一的地方调用await()时处理所有并发任务的错误。同时这也意味着如果你启动了一个async协程但从未调用await()其内部的异常可能会被静默忽略不推荐这样做。5. 核心区别三结构化并发与取消在结构化并发中协程之间存在父子关系。取消父协程会取消所有子协程。launch和async在取消响应上行为一致但async因为await()的存在在取消处理上需要额外注意。5.1 取消的传播fun main() runBlocking { val parentJob launch { println(Parent coroutine started) val childLaunch launch { repeat(10) { i - println(Child launch: Im alive $i) delay(500) } } val childAsync async { repeat(10) { i - println(Child async: Im alive $i) delay(500) } Async Result } delay(1500) // 让子协程运行一会儿 println(Parent: Cancelling children!) childLaunch.cancel() // 取消 launch 子协程 childAsync.cancel() // 取消 async 子协程 childLaunch.join() // 等待 launch 子协程结束 childAsync.join() // 等待 async 子协程结束 println(Parent: Children cancelled.) } parentJob.join() }在这个例子中无论是launch还是async创建的子协程在收到cancel()请求后都会停止执行。它们都是Job的子类遵循相同的取消协作机制例如在delay这样的挂起点检查isActive。5.2async与await()的取消一个常见的陷阱是如果一个async协程被取消那么调用它的await()会抛出CancellationException。fun main() runBlocking { val deferred async { delay(2000) Normal Result } delay(500) // 等一小会儿 deferred.cancel() // 在 async 完成前取消它 try { val result deferred.await() // 这里会抛出异常 println(Result: $result) } catch (e: CancellationException) { println(Await failed because the async was cancelled: $e) } }输出Await failed because the async was cancelled: kotlinx.coroutines.JobCancellationException: ...最佳实践当你使用async进行并发计算时务必考虑取消场景并在调用await()的地方妥善处理CancellationException。6. 实战案例并发网络请求与结果聚合这是async最经典的用武之地。假设我们需要从两个不同的 API 端点获取用户数据和文章列表然后合并展示。6.1 错误示范使用launch无法聚合结果// 假设的挂起函数模拟网络请求 suspend fun fetchUserData(): String { delay(1000) return {name: Alice, id: 123} } suspend fun fetchArticleList(): String { delay(1200) return [article1, article2, article3] } fun main() runBlocking { var userData: String? null var articles: String? null launch { userData fetchUserData() println(Fetched user: $userData) } launch { articles fetchArticleList() println(Fetched articles: $articles) } // 问题如何知道两个请求都完成了 // 问题即使使用 job.join() 等待也无法方便地拿到结果值。 delay(2000) // 粗暴地等待不可靠 println(All done? User: $userData, Articles: $articles) }这种方式非常笨拙且容易出错。我们无法优雅地等待两个任务完成并且获取结果的方式依赖于可变变量线程不安全虽然在单线程调度器中是安全的但模式不好。6.2 正确示范使用async并发执行suspend fun fetchUserData(): String { delay(1000) return {name: Alice, id: 123} } suspend fun fetchArticleList(): String { delay(1200) return [article1, article2, article3] } fun main() runBlocking { println(Starting concurrent requests...) val time measureTimeMillis { // 同时启动两个异步任务 val userDeferred: DeferredString async { fetchUserData() } val articlesDeferred: DeferredString async { fetchArticleList() } // 等待所有结果。await() 会挂起但两个请求是并发的。 val userData userDeferred.await() // 大约在第1秒完成 val articles articlesDeferred.await() // 大约在第1.2秒完成 println(User Data: $userData) println(Articles: $articles) } println(Total time taken: ${time}ms) // 总时间约1200ms而非2200ms }输出Starting concurrent requests... User Data: {name: Alice, id: 123} Articles: [article1, article2, article3] Total time taken: 1215ms优势分析并发执行两个async块几乎同时开始总耗时取决于最慢的那个任务~1200ms而不是两者之和~2200ms。结果聚合通过await()可以安全、方便地获取每个任务的结果。代码清晰逻辑清晰没有回调地狱也没有共享可变状态的烦恼。6.3 进阶使用awaitAll处理多个async任务当有多个并发任务时可以使用awaitAll函数。fun main() runBlocking { val deferredList listOf( async { doWork(Task1, 300L) }, async { doWork(Task2, 500L) }, async { doWork(Task3, 100L) } ) // awaitAll 会等待所有 Deferred 完成并返回一个结果列表 val results: ListString deferredList.awaitAll() println(All results: $results) }7. 核心区别四启动时机与惰性启动launch和async都支持CoroutineStart参数可以控制协程的启动时机尤其是CoroutineStart.LAZY。7.1 默认启动 (DEFAULT)默认情况下协程在构建器被调用时立即安排执行注意不是立即执行是进入调度队列。这是我们之前所有示例中使用的方式。7.2 惰性启动 (LAZY)当使用start CoroutineStart.LAZY时协程不会立即启动直到你主动触发它。对于launch触发方式是调用job.start()或job.join()。对于async触发方式是调用deferred.await()或deferred.start()。fun main() runBlocking { println(Start of program) val lazyJob launch(start CoroutineStart.LAZY) { println(Lazy launch executed) } val lazyDeferred async(start CoroutineStart.LAZY) { println(Lazy async executed) Lazy Result } println(Lazy coroutines created, but not started.) delay(1000) println(Now starting them...) lazyJob.start() // 启动 lazyJob val result lazyDeferred.await() // 启动 lazyDeferred 并获取结果 println(Async result: $result) }输出Start of program Lazy coroutines created, but not started. Now starting them... Lazy launch executed Lazy async executed Async result: Lazy Result使用场景惰性启动在你需要根据某些条件决定是否启动协程时非常有用可以避免不必要的资源开销。8. 常见问题与排查思路在实际使用launch和async时你可能会遇到以下典型问题。问题现象可能原因排查思路与解决方案launch内部的异常导致 App 崩溃未捕获的异常在launch中抛出并传播到顶级作用域。1. 在launch的代码块内部使用try/catch。2. 为协程作用域安装CoroutineExceptionHandler。3. 使用SupervisorJob防止异常在兄弟协程间传播。async启动后结果丢失或者异常被静默忽略创建了Deferred对象但从未调用await()。1. 确保每个async都有对应的await()调用以获取结果或捕获异常。2. 如果确实不需要结果考虑是否应该用launch代替。3. 对于一组async使用awaitAll()确保全部处理。并发任务没有变快还是顺序执行1. 在单线程调度器如Dispatchers.Main上运行。2. 错误地顺序调用了await()。1. 检查协程上下文。对于 CPU 密集型或 IO 密集型并发任务使用Dispatchers.Default或Dispatchers.IO。2. 确保先启动所有async再集中调用await()。错误示例val r1 async{...}.await(); val r2 async{...}.await()(这是顺序执行)。正确示例val d1 async{...}; val d2 async{...}; val r1 d1.await(); val r2 d2.await()。调用await()时抛出CancellationException在await()被调用前该Deferred对应的协程已被取消。1. 检查取消逻辑确保在等待结果前协程未被意外取消。2. 使用try/catch包裹await()调用处理CancellationException进行资源清理或回退操作。GlobalScope.launch/async导致内存泄漏在 Android 中使用GlobalScope启动的协程生命周期与 Application 一致如果协程持有 Activity/Fragment 的引用会导致它们无法被回收。最佳实践避免使用GlobalScope。使用生命周期感知的协程作用域如viewModelScope(ViewModel),lifecycleScope(Activity/Fragment)。这些作用域会在组件销毁时自动取消所有子协程。9. 最佳实践与工程建议理解了区别之后如何在项目中正确、高效地使用它们呢9.1 选择指南何时用launch何时用async使用launch当执行“一劳永逸”的后台任务且不需要返回结果。例如日志记录、分析事件上报、清理临时缓存、更新 UI 状态在 Android 主线程上。典型代码viewModelScope.launch { updateUiState() }使用async当需要并发执行多个任务并聚合它们的结果。需要获取一个后台计算任务的返回值。例如同时从多个接口获取数据、并行计算然后合并、执行一个需要返回值的耗时操作。典型代码val data1 async { repo.fetchData1() }; val data2 async { repo.fetchData2() }; val combined combine(data1.await(), data2.await())简单记忆要结果用async不要结果用launch。9.2 异常处理策略对于launch优先考虑在协程内部处理异常。如果异常需要向上报告使用CoroutineExceptionHandler。在 Android 的viewModelScope.launch中未捕获的异常可能导致 ViewModel 的后续操作中止务必小心。viewModelScope.launch(CoroutineExceptionHandler { _, exception - // 在这里处理或上报异常 Log.e(MyViewModel, Caught exception in launch, exception) }) { // 可能抛出异常的代码 riskyOperation() }对于async将异常处理逻辑放在调用await()的地方。这通常更清晰因为所有并发任务的错误可以集中处理。viewModelScope.launch { val deferred async { riskyOperationWithResult() } try { val result deferred.await() // 使用结果 } catch (e: Exception) { // 处理 async 任务中发生的异常 handleError(e) } }9.3 结构化并发与作用域管理永远避免使用GlobalScope除非你非常清楚它的生命周期与你的应用进程一致且不持有可能导致泄漏的引用。在 Android 中充分利用生命周期感知的作用域在ViewModel中执行后台操作使用viewModelScope。当 ViewModel 被清除时所有协程自动取消。在Activity或Fragment中执行与 UI 生命周期相关的操作使用lifecycleScope.launch并配合repeatOnLifecycle来确保协程只在特定生命周期状态如STARTED下运行避免后台更新无效 UI。对于自定义作用域记得在不再需要时调用scope.cancel()防止协程泄漏。9.4 性能与线程调度Dispatchers.Main用于更新 UI。launch(Dispatchers.Main) { ... }很常见。async在 UI 线程上做耗时计算会阻塞界面应避免。Dispatchers.IO用于网络请求、数据库操作、文件读写等 I/O 密集型任务。Dispatchers.Default用于 CPU 密集型计算如排序、解析复杂数据、图像处理等。正确选择调度器将async用于并发 CPU 计算时确保它们在Dispatchers.Default上运行。将async用于并发网络请求时使用Dispatchers.IO。// 好的实践并发网络请求 viewModelScope.launch { val userDeferred async(Dispatchers.IO) { api.getUser() } val postsDeferred async(Dispatchers.IO) { api.getPosts() } // await() 会挂起但不阻塞主线程 val user userDeferred.await() val posts postsDeferred.await() // 合并结果可能在主线程更新UI updateUi(user, posts) }掌握launch和async的区别是写出高效、健壮 Kotlin 协程代码的基石。希望这篇详细的对比能帮助你彻底理解它们并在项目中游刃有余地运用。记住核心原则launch用于“执行任务”async用于“计算并返回结果”结合结构化并发和恰当的异常处理你的异步代码将变得清晰而强大。