跳到主要内容

1 篇博文 含有标签「launch」

查看所有标签

Kotlin 协程入门实战:用 launch/async 告别回调地狱与线程管理

· 阅读需 6 分钟
重庆 Kotlin User Group

你有没有写过这样的代码?一个"先拿用户信息,再拿用户订单"的需求,硬是套了两层回调:

// 传统异步:回调地狱的入口
getUser("1001") { user ->
getOrders(user.id) { orders ->
getOrderDetail(orders.first().id) { detail ->
// 还想再嵌套?来啊
runOnUiThread { textView.text = detail.toString() }
}
}
}

每多一步异步操作,代码就往右缩进一格。写到第五层的时候,你已经开始怀疑人生了。而且你还得手动管理线程:哪个回调在子线程、哪个能更新 UI,一不小心就 CalledFromWrongThreadException。

Kotlin 协程(Coroutines)就是来终结这一切的。它最魔性的地方在于——让你用同步的写法,做异步的事。上面的代码改成协程是这样的:

lifecycleScope.launch {
val user = getUser("1001") // 看起来是同步,其实不阻塞
val orders = getOrders(user.id)
val detail = getOrderDetail(orders.first().id)
textView.text = detail.toString() // 自动回到主线程
}

一行套一行,读起来像流水账,但底层是异步非阻塞的。是不是很舒服?今天我们把它拆开讲明白。

协程到底是什么?​

一句话:协程是"轻量级的线程"。但它不是线程——线程是操作系统调度的,创建和切换都很贵;协程由 Kotlin 运行时管理,切换成本极低。

有多轻?下面这行开 10 万个协程毫无压力,但 10 万个线程直接就崩了:

repeat(100_000) {
launch { delay(1000) }
}

协程的核心能力是挂起(suspend)而不是阻塞(block)。当协程遇到一个耗时操作时,它不是傻等占着线程,而是"暂停自己、把线程让出去",等结果好了再回来继续。这就是为什么它能用同步写法实现异步效果。

suspend:标记"这里可以暂停"​

suspend 关键字用来标记一个挂起函数——它告诉编译器:"这个函数执行到一半可能会暂停,稍后再恢复。"

// 模拟一个耗时 1 秒的网络请求
suspend fun fetchUser(id: String): String {
delay(1000) // delay 是协程版的 sleep,非阻塞
return "用户$id"
}

这里有两个关键点,新手最容易踩:

  1. suspend 函数只能在协程或另一个 suspend 函数里调用。 你在普通的 main 函数里直接 fetchUser("1") 会编译报错。这是 Kotlin 在保护你——它得知道"在哪个协程里暂停、由谁来恢复"。
  2. suspend 不等于"自动切到子线程"。 挂起是暂停执行,切线程是调度器的事,两者是独立的。这点后面说。

那 main 里想调用怎么办?用 runBlocking 开一个协程作用域来"桥接":

import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
println(fetchUser("1001")) // 现在可以调了
}

launch 和 async:两种启动方式​

suspend 函数是"能暂停的操作",那谁来启动协程?两个最常用的构建器:launch 和 async。

launch:启动一个协程,不关心返回值,返回一个 Job(用来取消或等待)。适合"发出去就不用管"的任务,比如打日志、存数据。

val job = scope.launch {
saveToDatabase(data)
}
// 需要的话可以取消它
job.cancel()

async:启动一个协程,需要拿结果,返回一个 Deferred<T>(可以理解为"未来的结果"),用 .await() 取值。

val deferred = scope.async {
fetchUser("1001") // 返回 Deferred<String>
}
val user = deferred.await() // 拿到 "用户1001"

选择原则很简单:要结果用 async,不要结果用 launch。

经典踩坑:串行 ≠ 并行​

这是最多人踩的坑,也是协程最"哇"的知识点。假设你要同时请求用户和订单,然后合并:

// ❌ 错误写法:看着像并行,其实是串行
scope.launch {
val user = async { fetchUser("1001") }.await() // 等 1 秒
val orders = async { fetchOrders("1001") }.await() // 再等 1 秒
// 总耗时:2 秒
}

问题出在 .await():你每 async 完就立刻 await,等于"启动一个、马上等它结束、再启动下一个",退化成串行排队了。

正确做法是先把所有 async 发出去,再统一 await:

// ✅ 正确写法:真正的并行
scope.launch {
val userDeferred = async { fetchUser("1001") } // 同时开始
val ordersDeferred = async { fetchOrders("1001") } // 同时开始
val user = userDeferred.await()
val orders = ordersDeferred.await()
// 总耗时:1 秒(两个任务并行跑)
}

两个 async 一发出就都在跑了,await 只是去"取已经算好的结果"。这就是协程并发的正确姿势——先发射,后收集。