第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 两种入口以及挂起/恢复的路径。

协程生命周期流程图 开始 选择启动器 launch (无返回值) async (有返回值) 挂起函数调用 挂起 (Suspend) 恢复 (Resume) 结束 说明:挂起函数调用后协程立即进入“挂起”状态,释放底层线程;当挂起条件满足时,调度器将协程恢复在另一个可用线程上。

1.5 性能与稳定性优化建议

  • 避免全局协程泄漏:在 Android 的 Activity/Fragment、JVM 的 HTTP 请求等作用域中使用 viewModelScope 或自定义 CoroutineScope,并在生命周期结束时调用 cancel()。
  • 合理选择调度器:Dispatchers.IO 用于 IO 密集任务(文件、网络),Dispatchers.Default 用于 CPU 密集计算,切勿在 Main 线程执行长时间任务。
  • 挂起函数内避免全局锁:挂起函数本已非阻塞,若在内部使用 synchronized 或 ReentrantLock 会导致线程阻塞,破坏协程优势。
  • 结构化管理:始终使用 coroutineScope { } 或 supervisorScope { } 包裹并行子协程,确保子协程的异常不会影响父作用域。