第1章 协程基础概念与启动方式
1.1 协程的本质:轻量级线程
Kotlin 协程本质上是一种并发设计模式,它提供了一种在代码中直接编写异步逻辑的方式,而无需手动管理线程的生命周期。与操作系统线程相比,协程由开发者(通过 Kotlin 运行时)调度,而非由 OS 内核调度。这意味着协程可以在 JVM 堆上以对象形式存在,其创建、切换和销毁的成本远远低于线程。
核心差异对比表
| 维度 | 操作系统线程 | Kotlin 协程 |
|---|---|---|
| 创建成本 | 高,需要内核调用栈(MB 级别) | 极低,仅有对象分配开销(KB 级别) |
| 切换开销 | 涉及系统调用、上下文切换 | 纯用户态,仅保存恢复几个变量 |
| 可创建数量 | 受限于硬件,通常几千 | 百万级无压力 |
| 调度方式 | 抢占式,由 OS 调度 | 协作式,由 Kotlin 运行时调度 |
| 阻塞操作 | 阻塞线程,导致资源浪费 | 挂起协程,释放底层线程 |
底层原理一览:Kotlin 编译器通过 CPS(Continuation Passing Style)变换将挂起函数转换为状态机。每个挂起点对应一个状态分支,协程对象内部维护一个 Continuation 对象,用于在挂起后恢复执行。线程池(如 DefaultDispatcher)负责为活跃的协程提供执行线程。
1.2 启动方式:launch 与 async
Kotlin 提供两个核心构建器(Builder)来启动协程:launch 和 async。两者都返回一个“句柄”用以控制协程,但返回类型和用途截然不同。
launch:返回 Job 对象,不携带结果值,适用于“即发即忘”或需要独立完成的任务,如日志写入、数据刷新、UI 动画触发。
async:返回 Deferred<T> 对象,该对象继承自 Job,并持有最终结果。通过 await() 在挂起状态下获取计算结果,适用于需要并行计算并等待结果的场景。
// launch 启动示例
fun main() = runBlocking {
val job: Job = launch(Dispatchers.IO) {
// 模拟耗时 IO 操作
delay(1000L)
println("launch 任务完成")
}
job.join() // 等待协程结束
}
// async 启动示例
fun main() = runBlocking {
val deferred: Deferred<Int> = async(Dispatchers.Default) {
// 模拟计算
delay(500L)
42
}
val result: Int = deferred.await() // 挂起等待结果
println("async 返回结果: $result")
}
API 签名对比:launch 的 lambda 返回 Unit,而 async 的 lambda 可以返回任意类型 T。若在 launch 中需要返回值,必须通过外部可变状态或 Channel 间接传递,这增加了并发风险;async 则天然支持返回值传递。
常见错误:
- 未调用
await():async 启动后如果不调用await(),异常会被静默吞掉,不会传播到外层作用域。 - 在
launch内使用Thread.sleep():会阻塞底层线程,导致线程池性能下降。应始终使用delay()实现非阻塞等待。 - 丢失
Job引用:启动协程后不保存Job对象,导致后续无法取消或等待。
1.3 挂起函数的定义与调用
挂起函数(Suspending Function)是协程的核心抽象,使用 suspend 关键字修饰。挂起函数只能在协程体内或另一个挂起函数内调用。当挂起函数执行到 delay()、await() 或自定义挂起点时,当前协程会暂停执行并将底层线程归还给线程池,待条件满足后由调度器恢复执行。
// 定义挂起函数
suspend fun fetchUserData(): String {
delay(2000L) // 模拟网络请求
return "用户数据"
}
// 调用挂起函数
fun main() = runBlocking {
val data = fetchUserData() // 在协程内直接调用
println("获取到: $data")
}
调试贴士:在 Android 或 JVM 应用中,可以在 runBlocking 作用域外部添加 kotlinx.coroutines.debug 依赖,并在 VM 参数中添加 -Dkotlinx.coroutines.debug,打印日志时即可看到协程名称和所在线程信息:
[main] @coroutine#1 启动协程
[DefaultDispatcher-worker-1] @coroutine#2 执行挂起操作
1.4 协程生命周期流程
下图展示了从协程启动到结束的完整状态机流程,包含 launch / async 两种入口以及挂起/恢复的路径。
1.5 性能与稳定性优化建议
- 避免全局协程泄漏:在 Android 的 Activity/Fragment、JVM 的 HTTP 请求等作用域中使用
viewModelScope或自定义CoroutineScope,并在生命周期结束时调用cancel()。 - 合理选择调度器:
Dispatchers.IO用于 IO 密集任务(文件、网络),Dispatchers.Default用于 CPU 密集计算,切勿在Main线程执行长时间任务。 - 挂起函数内避免全局锁:挂起函数本已非阻塞,若在内部使用
synchronized或ReentrantLock会导致线程阻塞,破坏协程优势。 - 结构化管理:始终使用
coroutineScope { }或supervisorScope { }包裹并行子协程,确保子协程的异常不会影响父作用域。