Kotlin 协程设计思想(四):launch、async、withContext 到底有什么区别?
—— 从 Job、Deferred 到结构化并发彻底讲透 Kotlin 协程三大启动方式的设计思想前面三篇我们已经讲了CoroutineContext协程运行环境 Job协程生命周期管理器 Dispatcher协程调度策略到这里我们终于可以回头看一个高频问题launch、async、withContext 到底有什么区别很多教程会告诉你launch 没有返回值async 有返回值withContext 用来切线程这当然没错。但这只是最表层的理解。真正要理解它们必须回到协程的设计思想launch 代表启动一个任务async 代表启动一个带结果的任务withContext 代表在当前协程中切换运行环境这一篇我们就从Job、Deferred、结构化并发三个角度彻底讲透它们。一、先看最常见的 launchviewModelScope.launch { login() }launch的作用是启动一个新的协程任务它返回的是Job例如val job viewModelScope.launch { delay(3000) println(任务完成) }你可以job.cancel()也可以job.join() //等待这个协程执行完成所以launch 的核心不是返回结果 而是管理任务生命周期一句话launch 适合“只关心执行不关心结果”的任务例如提交日志 发送埋点 刷新页面 启动一个监听二、async 是什么再看val deferred viewModelScope.async { getUserInfo() }async也会启动一个新的协程。但它返回的不是普通 Job而是DeferredT例如val userDeferred async { getUserInfo() } val user userDeferred.await()await()可以拿到结果。所以Deferred 带结果的 Job它既能取消deferred.cancel()也能等待结果val result deferred.await()所以async 适合“并发执行并且需要结果”的任务三、Job 和 Deferred 的关系可以这样理解Job 只表示一个任务 Deferred 表示一个有结果的任务关系类似Job - 任务 DeferredT - 任务 结果所以launch { }返回Job而async { }返回DeferredT这就是它们最核心的区别。四、async 最经典的场景并发请求例如viewModelScope.launch { val userDeferred async { api.getUser() } val orderDeferred async { api.getOrders() } val user userDeferred.await() val orders orderDeferred.await() updateUI(user, orders) }这里两个请求是并发执行而不是一个执行完再执行另一个如果每个请求 1 秒串行需要2 秒并发大约1 秒这就是 async 的价值。五、那 withContext 呢很多人这样写val user withContext(Dispatchers.IO) { api.getUser() }然后理解成切到 IO 线程没错但不完整。withContext的本质是在当前协程中切换 CoroutineContext它不会像launch、async那样开启一个并列的新任务。它是挂起当前协程 ↓ 切换 Context 执行代码块 ↓ 执行完恢复回来所以viewModelScope.launch { val user withContext(Dispatchers.IO) { api.getUser() } updateUI(user) }执行顺序是进入 launch ↓ 切到 IO 执行 getUser ↓ 拿到 user ↓ 回到原协程继续 updateUI一句话withContext 适合“当前流程中某一段代码需要切换运行环境”六、launch 和 withContext 最大区别看起来都能切线程launch(Dispatchers.IO) { api.getUser() }withContext(Dispatchers.IO) { api.getUser() }但它们完全不同。launchlaunch(Dispatchers.IO) { api.getUser() }含义启动一个新的协程调用后不会等待它执行完除非你手动job.join()withContextwithContext(Dispatchers.IO) { api.getUser() }含义切换当前协程的运行环境调用后会等待代码块执行完然后继续往下走。所以可以记launch开新任务 withContext切换当前任务的执行环境七、async 和 withContext 都能返回结果区别是什么例如val user withContext(Dispatchers.IO) { api.getUser() }也能返回结果。val user async { api.getUser() }.await()也能返回结果。那区别是什么关键在于async 是并发模型 withContext 是顺序模型withContextval user withContext(Dispatchers.IO) { api.getUser() } val orders withContext(Dispatchers.IO) { api.getOrders() }执行顺序先 getUser 再 getOrders这是串行。asyncval userDeferred async { api.getUser() } val ordersDeferred async { api.getOrders() } val user userDeferred.await() val orders ordersDeferred.await()执行顺序getUser 和 getOrders 同时开始这是并发。所以withContext我要切线程并等待结果 async我要并发执行并等待结果八、为什么 async 不建议单独使用很多人会写val result async { api.getUser() }.await()这通常没有意义。因为启动 async 立刻 await等价于并没有形成并发这时候更推荐val result withContext(Dispatchers.IO) { api.getUser() }所以async 的价值在于并发 不是单纯返回结果九、异常处理有什么不同这是很多人踩坑的地方。launch 的异常viewModelScope.launch { throw RuntimeException(error) }launch中未捕获异常会直接抛给父协程。如果父协程没有处理可能导致整个作用域取消。async 的异常val deferred async { throw RuntimeException(error) }async的异常会先保存在Deferred里。直到你调用deferred.await()异常才会重新抛出来。例如try { val result deferred.await() } catch (e: Exception) { e.printStackTrace() }所以async 的异常通常在 await 时暴露十、结构化并发下的 async很多人以为async就是随便开一个后台任务。不是。在结构化并发里viewModelScope.launch { val user async { api.getUser() } val orders async { api.getOrders() } updateUI(user.await(), orders.await()) }这两个 async 都是launch 的子协程结构Parent launch │ ├── async user └── async orders如果 parent 被取消两个 async 也会被取消。这就是结构化并发十一、coroutineScope 和 async 的经典组合如果你在 suspend 函数里想并发请求通常可以这样写suspend fun loadHomeData(): HomeData coroutineScope { val userDeferred async { api.getUser() } val bannerDeferred async { api.getBanner() } val user userDeferred.await() val banner bannerDeferred.await() HomeData(user, banner) }为什么要用coroutineScope因为它提供一个父作用域。确保所有子协程完成后 整个函数才返回如果其中一个失败其它子协程也会被取消这就是结构化并发。十二、supervisorScope 又是什么如果你希望一个请求失败不影响其它请求可以用supervisorScope { val userDeferred async { api.getUser() } val bannerDeferred async { api.getBanner() } val user runCatching { userDeferred.await() }.getOrNull() val banner runCatching { bannerDeferred.await() }.getOrNull() HomeData(user, banner) }区别是coroutineScope一个子协程失败整个作用域失败supervisorScope一个子协程失败不影响其它子协程这就和前面讲的Job SupervisorJob完全串起来了。十三、三者怎么选可以这样记。launch用于我只想启动一个任务 不需要返回结果例如viewModelScope.launch { refresh() }async用于我想并发执行多个任务 并且需要它们的结果例如val user async { api.getUser() } val orders async { api.getOrders() } combine(user.await(), orders.await())withContext用于我在当前流程中 需要切换 Dispatcher 并拿到结果例如val user withContext(Dispatchers.IO) { api.getUser() }十四、最终总结如果让我一句话解释launch我会说启动一个不关心返回值的协程任务。如果让我解释async我会说启动一个带返回值、适合并发组合的协程任务。如果让我解释withContext我会说在当前协程中切换运行环境并等待结果返回。它们背后的关系是launch - Job async - DeferredT withContext - 切换 CoroutineContext所以launch 解决任务启动 async 解决并发结果 withContext 解决上下文切换真正理解这三者就不是背 API 区别 而是理解 Kotlin 协程的任务模型。下篇预告到这里我们已经讲完了CoroutineContext Job Dispatcher launch / async / withContext但协程里还有一个最容易让人崩溃的问题异常处理。为什么try-catch有时候能捕获有时候不能为什么CoroutineExceptionHandler有时候生效有时候不生效为什么async的异常非要等到await()才暴露下一篇我们继续《Kotlin 协程设计思想五协程异常为什么这么难理解》从 launch、async、SupervisorJob 到 CoroutineExceptionHandler彻底讲透 Kotlin 协程异常传播机制。