Kotlin 协程设计思想(四):launch、async、withContext 到底有什么区别?

------ 从 Job、Deferred 到结构化并发,彻底讲透 Kotlin 协程三大启动方式的设计思想

前面三篇

Kotlin 协程设计思想(一):CoroutineContext 到底是什么?为什么 Job 和 Dispatcher 可以直接相加?-CSDN博客

Kotlin 协程设计思想(二):Job 到底是什么?为什么协程能被取消?-CSDN博客

Kotlin 协程设计思想(三):Dispatchers 到底是什么?切线程真的只是切线程吗?-CSDN博客

我们已经讲了:

复制代码
CoroutineContext:协程运行环境
Job:协程生命周期管理器
Dispatcher:协程调度策略

到这里,我们终于可以回头看一个高频问题:

复制代码
launch、async、withContext 到底有什么区别?

很多教程会告诉你:

launch 没有返回值

async 有返回值

withContext 用来切线程

这当然没错。但这只是最表层的理解。

真正要理解它们,必须回到协程的设计思想:

launch 代表启动一个任务

async 代表启动一个带结果的任务

withContext 代表在当前协程中切换运行环境

这一篇,我们就从 Job、Deferred、结构化并发三个角度,彻底讲透它们。


一、先看最常见的 launch

Kotlin 复制代码
viewModelScope.launch {
    login()
}

launch 的作用是:启动一个新的协程任务

它返回的是:Job

例如:

Kotlin 复制代码
val job = viewModelScope.launch {
    delay(3000)
    println("任务完成")
}

你可以:job.cancel()

也可以:job.join() //(等待这个协程执行完成)

所以:

复制代码
launch 的核心不是返回结果

而是管理任务生命周期

一句话:

复制代码
launch 适合"只关心执行,不关心结果"的任务

例如:

复制代码
提交日志

发送埋点

刷新页面

启动一个监听

二、async 是什么?

再看:

Kotlin 复制代码
val deferred = viewModelScope.async {
    getUserInfo()
}

async 也会启动一个新的协程。

但它返回的不是普通 Job,

而是:

复制代码
Deferred<T>

例如:

Kotlin 复制代码
val userDeferred = async {
    getUserInfo()
}

val user = userDeferred.await()

await() 可以拿到结果。

所以:

复制代码
Deferred = 带结果的 Job

它既能取消:

复制代码
deferred.cancel()

也能等待结果:

复制代码
val result = deferred.await()

所以:

复制代码
async 适合"并发执行,并且需要结果"的任务

三、Job 和 Deferred 的关系

可以这样理解:

复制代码
Job
只表示一个任务

Deferred
表示一个有结果的任务

关系类似:

复制代码
Job        -> 任务
Deferred<T> -> 任务 + 结果

所以:

复制代码
launch { }

返回:

复制代码
Job

而:

复制代码
async { }

返回:

复制代码
Deferred<T>

这就是它们最核心的区别。


四、async 最经典的场景:并发请求

例如:

Kotlin 复制代码
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 呢?

很多人这样写:

Kotlin 复制代码
val user = withContext(Dispatchers.IO) {
    api.getUser()
}

然后理解成:

复制代码
切到 IO 线程

没错,

但不完整。

withContext 的本质是:

复制代码
在当前协程中切换 CoroutineContext

它不会像 launchasync 那样开启一个并列的新任务。

它是:

复制代码
挂起当前协程
↓
切换 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()
}

但它们完全不同。


launch

复制代码
launch(Dispatchers.IO) {
    api.getUser()
}

含义:

复制代码
启动一个新的协程

调用后:

复制代码
不会等待它执行完

除非你手动:

复制代码
job.join()

withContext

复制代码
withContext(Dispatchers.IO) {
    api.getUser()
}

含义:

复制代码
切换当前协程的运行环境

调用后:

复制代码
会等待代码块执行完

然后继续往下走。


所以可以记:

复制代码
launch:开新任务

withContext:切换当前任务的执行环境

七、async 和 withContext 都能返回结果,区别是什么?

例如:

Kotlin 复制代码
val user = withContext(Dispatchers.IO) {
    api.getUser()
}

也能返回结果。

Kotlin 复制代码
val user = async {
    api.getUser()
}.await()

也能返回结果。

那区别是什么?

关键在于:

复制代码
async 是并发模型

withContext 是顺序模型

withContext

复制代码
val user = withContext(Dispatchers.IO) {
    api.getUser()
}

val orders = withContext(Dispatchers.IO) {
    api.getOrders()
}

执行顺序:

复制代码
先 getUser
再 getOrders

这是串行。


async

复制代码
val 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()

异常才会重新抛出来。

例如:

Kotlin 复制代码
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 又是什么?

如果你希望:

一个请求失败,不影响其它请求

可以用:

Kotlin 复制代码
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 -> Deferred<T>

withContext -> 切换 CoroutineContext

所以:

复制代码
launch 解决任务启动

async 解决并发结果

withContext 解决上下文切换

真正理解这三者,就不是背 API 区别, 而是理解 Kotlin 协程的任务模型。


下篇预告

到这里,我们已经讲完了:

复制代码
CoroutineContext
Job
Dispatcher
launch / async / withContext

但协程里还有一个最容易让人崩溃的问题:

复制代码
异常处理。

为什么:

复制代码
try-catch

有时候能捕获,有时候不能?

为什么:

复制代码
CoroutineExceptionHandler

有时候生效,有时候不生效?

为什么:

复制代码
async

的异常非要等到:

复制代码
await()

才暴露?

下一篇我们继续:

《Kotlin 协程设计思想(五):协程异常为什么这么难理解?》

从 launch、async、SupervisorJob 到 CoroutineExceptionHandler,

彻底讲透 Kotlin 协程异常传播机制。

相关推荐
日取其半万世不竭1 小时前
Uptime Kuma 应该放哪台机器?
java·docker·容器·https
夜白宋1 小时前
【Redis深入】二、高性能
java·前端·redis
空圆小生1 小时前
Vue3 + Spring Boot 全栈实战:从零搭建在线彩票模拟系统
java·spring boot·后端
devpotato1 小时前
ArrayList 扩容机制:从源码细节到工程实践
java·list
修行者对6661 小时前
Kotlin学习笔记(1)
kotlin
运维瓦工1 小时前
DevOps 生态介绍(八):docker &dockerfile 命令介绍及构建项目的第一个镜像
java·docker·devops
yurenpai(27届找实习中)1 小时前
Spring AI 实战:从零实现 AI 对话的记忆与历史记录管理(附源码级解析)
java·spring·ai·prompt·word
nnsix1 小时前
Unity 自定义包的 package.json 简单写法
java·服务器·前端
宸津-代码粉碎机1 小时前
Spring AI企业级RAG进阶|文档智能分片调优、ES深度整合、接口限流熔断监控生产实战
java·开发语言·人工智能·后端·spring·elasticsearch·oracle