协程是什么:比线程更轻的并发单元
线程是操作系统调度的单位,开一个线程要占不少内存。协程则是 Kotlin 在用户态实现的"轻量线程"——你可以同时开成千上万个协程,它们跑在少数几个真正的线程上,由 Kotlin 自己调度。协程的核心价值:写异步代码像写同步一样顺,但不用真的占着线程死等。
对比一下:用 Thread 做网络请求,你得写回调、锁、join,层层嵌套;用协程,你直接写 val data = api.fetch()——看起来是同步的,但底层线程在等网络时去做别的事,结果回来再接着跑。
协程的本质:把"等待 I/O"这件事变成"挂起—恢复",线程不被占住,代码却读起来像同步。
- launch起一个不返回结果的协程,即发即忘。
- suspend标记一个函数可以被挂起,只能在协程里调。
- async起一个带返回值的协程,用 await 拿结果。
- Flow把多个异步结果像流水线一样逐个发出来。
launch 与 runBlocking:怎么起一个协程
协程不能凭空跑,必须有一个"调度器"把它丢到线程上。最基础的启动方式是 runBlocking(测试用,会阻塞当前线程直到里面跑完)和 launch(真正的业务用,非阻塞)。
import kotlinx.coroutines.*
fun main() = runBlocking { // 阻塞 main 等里面跑完
launch { // 起一个协程
delay(1000) // 挂起 1 秒,不占线程
println("World")
}
print("Hello ")
}
这里 delay(1000) 是个 suspend 函数:它挂起协程 1 秒,期间线程去干别的。你不会真的卡 1 秒——和 Thread.sleep 本质不同。
suspend:标记一个可挂起的函数
你自己写的函数如果里面要 delay、要 await 网络结果,就给它加 suspend 前缀。这个关键字告诉编译器:"这个函数可能会挂起,只能在协程里调用"。
suspend fun fetchUser(): User {
delay(500) // 模拟网络延迟
return User("Alice")
}
// 只能在协程里调
runBlocking {
val u = fetchUser()
println(u.name)
}
关键心智:suspend 不会阻塞线程。它只是说"我可能要等一会儿,等的时候别占着线程"。这和回调地狱里"等完了调回调"是同一件事,只不过 Kotlin 把它写成了顺序代码。
async/await:并发拿结果
launch 不返回结果。要同时干两件事、最后汇总,用 async——它返回一个 Deferred 对象,用 await() 拿结果。
suspend fun loadBoth(): Pair<String, String> {
val a = async { loadUser() } // 开始跑,但不阻塞
val b = async { loadOrder() } // 同时开始跑
return a.await() to b.await() // 等两个都好
}
注意顺序:两个 async 必须先都"启动",再一起 await。如果你写成 a.await() 后才起 async { loadOrder() },那两个就是串行跑了——这点和 JavaScript 的 Promise.all 类似。
Flow:把一组异步数据流接住
协程解决"一次异步调用",Flow 解决"一连串异步值"——比如分页加载、实时推送、传感器数据流。它像一个可以异步产出多个元素的序列。
import kotlinx.coroutines.flow.*
fun numbers(): Flow<Int> = flow {
for (i in 1..3) {
delay(100) // 每发一个等 100ms
emit(i) // 吐出一个值
}
}
fun main() = runBlocking {
numbers().collect { println(it) } // 逐个接住
}
flow { } 块里用 emit 产出值,外面用 collect 消费。中间还能用 map、filter 像 RxJava 那样串操作符——但底层是协程,没有 Rx 那种笨重的线程模型。
协程作用域:谁来管生命周期
协程需要一个"作用域"来管它的生命周期:界面关了,正在跑的网络请求应该自动取消,不然就泄漏了。Android 里常见 viewModelScope、lifecycleScope——ViewModel 销毁时里面所有协程自动取消。
class MyViewModel : ViewModel() {
fun load() {
viewModelScope.launch { // 跟着 ViewModel 活
val data = fetch()
_uiState.value = data
}
}
}
记住一条:**永远在一个明确的 Scope 里起协程**,别在全局 GlobalScope 里乱起——那样你没法取消它,泄漏就在所难免。
实战:并发拉两个接口再合并
串起来:在 ViewModel 里并发拉用户信息和订单列表,两个都回来后更新界面。
viewModelScope.launch {
val userDeferred = async { api.getUser() }
val orderDeferred = async { api.getOrders() }
val user = userDeferred.await()
val orders = orderDeferred.await()
_uiState.value = UiState.Ready(user, orders)
}
两个请求并发跑,总耗时约等于慢的那个,而不是两个相加。从外面看代码是顺序写的,但底层网络等待时线程没被占住——这就是协程在 Android 日常开发里最常见的姿态。
协程检查清单
把 Kotlin 协程记成一条链:launch 起协程 → suspend 标可挂起 → async 并发拿值 → Flow 接数据流 → Scope 管生命周期。提交前对照自查:
- 协程是不是在明确的 Scope(viewModelScope 等)里起的,没有用 GlobalScope?
- 需要并发的地方是不是先 async 启动再 await,而不是串行 await?
- suspend 函数里有没有做阻塞操作(比如同步 sleep、大计算)?应该挪到 Dispatchers.IO。
- Flow 是不是会自动取消(collect 在 Scope 里),不会泄漏?
- 错误处理是不是用了 try/catch 或 flow 的 catch 操作符,没让异常崩掉界面?
Kotlin 协程的魅力:把异步写得像同步,把线程让给别人用,把生命周期交给 Scope 管。三件事各归各位,代码就既快又稳。
下一篇跳出移动端,看看 PHP 的现代框架 Laravel 是怎么用 MVC 一套把 Web 开发做得又快又规整的。