Kotlin 协程
协程(Coroutine)是 Kotlin 最强大的特性之一——用同步的写法做异步的事。它是 Android 异步编程的事实标准,也广泛用于服务端(Spring、Ktor)。协程极轻量(一个应用跑十万个协程没问题),无回调地狱,学习曲线比 RxJava 平缓得多。本篇系统讲。
1. 协程基础
用协程前先加依赖:org.jetbrains.kotlinx:kotlinx-coroutines-core。最小化示例:
// 协程:轻量级异步方案,用同步的写法做异步的事
// 需要先添加依赖:org.jetbrains.kotlinx:kotlinx-coroutines-core
import kotlinx.coroutines.*
fun main() = runBlocking { // runBlocking 启动顶层协程作用域
launch { // 启动一个新协程
delay(1000) // 非阻塞延迟(挂起 1 秒)
println("一秒后")
}
println("立即打印")
}
// 输出:
// 立即打印
// 一秒后
// (然后 main 结束)核心概念:runBlocking 启动顶层作用域(阻塞当前线程,常用于 main 函数和测试);launch 启动一个新协程(返回 Job,不返回结果);delay 是非阻塞延迟(挂起协程,不阻塞线程)。
2. suspend 挂起函数(关键概念)
suspend 是协程最重要的关键字。suspend 函数只能在协程或其他 suspend 函数里调用:
// suspend 函数:挂起函数,只能在协程里调用
// 【不会阻塞线程】,而是【挂起协程】,释放线程给其他协程用
suspend fun doWork(): String {
delay(500) // 挂起 500ms,不阻塞
return "结果"
}
suspend fun fetchUser(id: Int): User {
delay(300) // 模拟网络请求
return User(id, "用户$id")
}
// suspend 函数可以调用其他 suspend 函数
suspend fun loadData(): User {
val user = fetchUser(1) // 调用其他 suspend
println("加载完成: $user")
return user
}
// 普通 fun 不能调用 suspend 函数,会编译错误
// fun main() {
// delay(100) // 错误!delay 是 suspend
// }
//
// 必须用 suspend fun 或在协程里调用关键理解:suspend 不是阻塞——它会挂起协程,释放当前线程给其他协程用,等条件满足后恢复。这是协程比线程高效的核心机制。
底层原理:每个 suspend 函数会被编译成状态机,在挂起点保存局部变量,恢复时还原。这种"代码看起来同步、底层完全异步"的设计,叫"continuation-passing style 变换"。
3. launch vs async
启动协程的两个主要 builder:
// launch:启动一个【不返回结果】的协程(类似 fire-and-forget)
// async:启动一个【返回结果】的协程,返回 Deferred<T>
import kotlinx.coroutines.*
fun main() = runBlocking {
// launch 的"任务"
val job1 = launch {
delay(500)
println("任务1完成")
}
// async 的"任务",返回 Deferred
val deferred = async {
delay(300)
42 // 返回值
}
println("等待 async 结果...")
val result = deferred.await() // 等待 async 完成
println("async 结果: $result")
job1.join() // 等待 job1 完成
}
// 输出:
// 等待 async 结果...
// 任务1完成
// async 结果: 42
// await() 是 suspend,只能在协程里调用- launch:fire-and-forget(发射后不管),返回
Job。用于"我只想执行,不关心返回值"。 - async:返回
Deferred<T>(类似 Promise/Future),用.await()拿结果。用于"我需要异步计算的结果"。
经验:优先用 launch,只在需要返回值时用 async。async 的异常只在 await 时抛出,容易遗漏处理。
4. 并行 vs 串行(协程的杀手锏)
多个独立的异步任务用 async 并行,总耗时 = max(各任务)而非累加:
// 并行执行多个任务
import kotlinx.coroutines.*
fun main() = runBlocking {
// 串行:总耗时 = 任务A + 任务B + 任务C
val serialTime = measureTimeMillis {
val a = fetchData("A") // 100ms
val b = fetchData("B") // 200ms
val c = fetchData("C") // 150ms
}
println("串行: ${serialTime}ms") // ~450ms
// 并行:总耗时 = max(A, B, C)
val parallelTime = measureTimeMillis {
val a = async { fetchData("A") }
val b = async { fetchData("B") }
val c = async { fetchData("C") }
println(a.await() + b.await() + c.await())
}
println("并行: ${parallelTime}ms") // ~200ms
}
suspend fun fetchData(name: String): String {
delay(when (name) {
"A" -> 100
"B" -> 200
"C" -> 150
else -> 0
})
return name
}这就是协程的核心价值——异步并行代码看起来和同步串行代码一样清晰。对比传统的回调写法或 Future 链,这种"看起来同步、实际并行"的代码可读性极强。
5. Dispatchers(调度器)
调度器决定协程在哪个线程跑。Kotlin 内置 4 个:
// Dispatchers:协程调度器,决定协程在哪个线程跑
import kotlinx.coroutines.*
fun main() = runBlocking {
// Main:主线程(Android UI 线程)
launch(Dispatchers.Main) {
println("Main: ${Thread.currentThread().name}")
}
// IO:IO 线程池(网络、磁盘、数据库)
launch(Dispatchers.IO) {
println("IO: ${Thread.currentThread().name}")
}
// Default:CPU 密集型线程池(计算、排序、解析)
launch(Dispatchers.Default) {
println("Default: ${Thread.currentThread().name}")
}
// Unconfined:不限制,继承调用方线程(不推荐常用)
launch(Dispatchers.Unconfined) {
println("Unconfined: ${Thread.currentThread().name}")
}
// withContext:切换上下文(常用)
val result = withContext(Dispatchers.IO) {
// 这个块在 IO 线程跑
fetchData("X")
}
// 回到原线程
println("结果: $result")
}- Main:UI 线程(Android 主线程、Swing、JavaFX)。
- IO:IO 线程池(网络、磁盘、数据库),最多 64 个线程。
- Default:CPU 线程池(计算、解析、排序),线程数 = CPU 核数。
- Unconfined:不指定,继承调用方(高级用法,慎用)。
withContext 用于切换上下文:withContext(Dispatchers.IO) { fetchData() } 切到 IO 线程跑 fetchData,完成后自动切回。这是 Android 老套路:withContext(IO) 取数据 → withContext(Main) 更新 UI。
6. 协程作用域(structured concurrency)
这是 Kotlin 协程的核心设计——每个协程都属于一个作用域,作用域结束会自动取消所有子协程:
// CoroutineScope:协程作用域,管理协程的生命周期
// 作用域结束时,内部所有协程会自动取消
// 自定义作用域(用工厂方法)
val scope = CoroutineScope(Dispatchers.Main)
scope.launch {
// 这个协程受 scope 管理
}
// structured concurrency(结构化并发):
// 子协程的失败会传播到父协程,父协程取消会取消所有子协程
class MyService {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
fun startWork() {
scope.launch { /* ... */ }
}
fun destroy() {
scope.cancel() // 取消所有子协程
}
}
// runBlocking 是顶层作用域(用于 main、测试)
// GlobalScope 谨慎用:生命周期和应用一样长,易内存泄漏结构化并发的好处:
- 不能忘记取消:Activity 销毁时,scope.cancel() 自动取消所有协程,杜绝内存泄漏。
- 异常传播:子协程的异常会传到父协程,不会"丢异常"。
- 等待所有子协程完成:父协程会等所有子协程结束。
Android 用 lifecycleScope(Jetpack)、viewModelScope,它们的 scope 绑定到生命周期,自动取消。
7. Flow(协程版的数据流)
Flow 是协程版的"流",类似 RxJava 的 Observable 但更简洁。用于异步、按需、流式的数据生产:
// Flow:协程版的"流"(类似 RxJava 的 Observable)
// 用于【异步、按需、流式】的数据
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun numbers(): Flow<Int> = flow {
for (i in 1..5) {
delay(300) // 模拟异步生产
emit(i) // 发射一个值
}
}
fun main() = runBlocking {
// 收集 Flow
numbers().collect { value ->
println("收到: $value")
}
// 输出(每 300ms 一个):
// 收到: 1
// 收到: 2
// ...
// Flow 的操作符(类似 RxJava)
val result = numbers()
.map { it * it } // 转换
.filter { it > 4 } // 过滤
.take(2) // 取前 2 个
.toList()
println(result) // [9, 16]Flow 是冷流(cold flow)——只有 collect 时才会真正开始生产,且每个 collector 各生产一份。这避免了 RxJava 的某些坑。
常用操作符:map、filter、take、flatMapLatest、debounce、distinctUntilChanged 等。还有StateFlow(状态)、SharedFlow(事件总线),是 Android 替代 LiveData 的事实标准。
8. 异常处理
// 异常处理:协程异常会传播
import kotlinx.coroutines.*
fun main() = runBlocking {
// try-catch 可以捕获子协程的异常
try {
val job = launch {
throw RuntimeException("协程出错")
}
job.join()
} catch (e: Exception) {
println("捕获: ${e.message}")
}
// async 的异常只在 await() 时抛出
val deferred = async {
delay(100)
throw RuntimeException("async 出错")
}
try {
deferred.await()
} catch (e: Exception) {
println("捕获: ${e.message}")
}
// CoroutineExceptionHandler:全局异常处理器(用于 launch)
val handler = CoroutineExceptionHandler { _, e ->
println("未处理异常: ${e.message}")
}
launch(handler) {
throw RuntimeException("未捕获")
}
}几个关键点:
- launch 的异常直接抛出到父作用域的 CoroutineExceptionHandler。
- async 的异常在 await() 时抛出——容易遗漏,要记得 try-catch。
- SupervisorJob:让子协程的异常不传染到兄弟协程(适合"独立任务")。
- try-catch:可以包住 collect / await,处理异常。
- 不要在 suspend 函数里 catch 异常"吞掉":至少日志,否则 bug 难查。
9. 取消与超时
import kotlinx.coroutines.*
fun main() = runBlocking {
// 超时
val result = withTimeoutOrNull(500) {
repeat(10) {
delay(200)
println("tick $it")
}
"完成"
}
println("结果: $result") // 结果: null(超时了)
// 主动取消
val job = launch {
repeat(100) { i ->
delay(100)
println("工作 $i")
}
}
delay(350)
job.cancel() // 取消
job.join() // 等待真的结束
println("已取消")
// 协程代码要"配合"取消:
// delay() / 协程 API 都会检查取消
// 但 CPU 密集型代码要手动 yield()
suspend fun heavyWork() {
for (i in 1..1_000_000) {
yield() // 主动让出,允许取消
// 或者 ensureActive() / isActive 检查
}
}
}结构化并发的一个优势:取消是协作式的——协程代码主动检查取消(通过 suspend 函数),可以优雅清理资源(关闭文件、回滚事务)。
10. 实战:并行加载多接口
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.measureTimeMillis
data class User(val id: Int, val name: String)
data class Profile(val bio: String)
data class Avatar(val url: String)
suspend fun fetchUser(id: Int): User {
delay(200) // 模拟 API
return User(id, "用户$id")
}
suspend fun fetchProfile(id: Int): Profile {
delay(150)
return Profile("这是用户 $id 的简介")
}
suspend fun fetchAvatar(id: Int): Avatar {
delay(300)
return Avatar("https://example.com/$id.png")
}
fun main() = runBlocking {
val time = measureTimeMillis {
// 三个接口并行请求
val userDeferred = async { fetchUser(42) }
val profileDeferred = async { fetchProfile(42) }
val avatarDeferred = async { fetchAvatar(42) }
// 等所有结果(总耗时 ≈ 300ms,而不是 200+150+300=650ms)
val user = userDeferred.await()
val profile = profileDeferred.await()
val avatar = avatarDeferred.await()
println("User: $user")
println("Profile: $profile")
println("Avatar: $avatar")
}
println("总耗时: ${time}ms") // ~300ms
}这是典型的"并行加载多个独立接口"模式——一个页面的数据来自多个 API,并行请求可以大幅缩短加载时间。如果用传统回调写,代码嵌套三层、难维护;用协程写,看起来和串行代码一样清晰。
11. 协程 vs 线程 vs RxJava
- vs 线程:协程极轻量(1MB 栈 vs 几 KB),可以开十万个协程,线程只能开几千个。协程代码"看起来同步",线程需要回调或 Future。
- vs RxJava:协程学习曲线平缓,标准库支持,Kotlin 原生。RxJava 操作符丰富(背压、组合复杂流)但学习陡峭。新项目优先协程 + Flow。
- vs JavaScript async/await:概念类似,但 Kotlin 协程更强大(支持 structured concurrency、多调度器、Flow)。
- vs Java CompletableFuture:CompletableFuture 是 Future 的增强,但仍偏函数式。协程是"用同步代码写异步"的范式,可读性更好。
小结
这一章你掌握了 Kotlin 协程的全部核心:suspend 函数、launch/async、Dispatchers 调度器、CoroutineScope 结构化并发、Flow 数据流、异常处理、取消超时。协程是 Kotlin 区别于 Java 的最大杀手锏之一——一旦习惯,你会觉得 Java 的回调/CompletableFuture 都是噪音。
恭喜!你已经完成了整个 Kotlin 16 篇入门系列。从最简单的 val/var 到协程,你已经掌握了 Android 官方语言的全部核心特性。下一步:写一个 Android 小项目、读 Kotlin 源码、深入 Jetpack Compose 或 Ktor 服务端。Kotlin 是一门"实用主义"的语言——它不追求理论上的纯粹,而是把各种语言里最好用的特性整合到一起,让开发者写得爽、出错少。
← 上一篇 Kotlin Lambda 与高阶函数
返回 Kotlin 教程目录