2. Kotlin协程

一灰灰blogKotlinKotlin约 2190 字大约 7 分钟

以下是关于 Kotlin 协程的深度解析,包含核心概念、线程对比、使用方式、工作原理及最佳实践:

一、协程的本质与核心概念

定义:协程是一种轻量级的线程替代方案,由程序控制调度,而非操作系统。它允许代码暂停(suspend)和恢复执行,无需阻塞线程。

核心特性

  • 轻量级:单个线程可运行数千个协程,内存占用仅 ~1KB(对比线程的 MB 级)。
  • 非阻塞:协程挂起时不阻塞线程,线程可执行其他任务。
  • 结构化并发:通过作用域管理协程生命周期,避免内存泄漏。
  • 挂起函数:使用 suspend 标记的函数,可暂停和恢复执行。

二、协程 vs 线程

特性协程(Coroutine)线程(Thread)
调度由程序(协程调度器)控制由操作系统内核调度
创建成本极低(约 1KB 内存)高(约 1MB 内存,视平台而定)
切换开销极小(纳秒级,仅涉及上下文切换)高(微秒级,涉及内核态切换)
并发性单线程可运行数千个协程受限于系统资源(通常数百个)
阻塞影响仅挂起当前协程,不影响线程阻塞整个线程,其他任务需等待
适用场景I/O 密集型任务(如网络请求)CPU 密集型任务(如计算)

三、协程的基本使用姿势

1. 启动协程的方式

所有的协程必须在一个作用域内执行,使用方式为

作用域.launch {}

其中 launch 是一个函数,用于创建协程并将其函数主体的执行分派给相应的调度程序

Dispatchers.IOopen in new window 指示此协程应在为 I/O 操作预留的线程上执行。

一个基础的使用示例:

import kotlinx.coroutines.*

// 1. GlobalScope.launch:非结构化,谨慎使用
GlobalScope.launch {
    delay(1000) // 非阻塞延迟
    println("GlobalScope 协程")
}

// 2. runBlocking:阻塞当前线程,用于测试
runBlocking {
    launch { // 默认继承 runBlocking 的协程作用域
        println("runBlocking 内部协程")
    }
}

// 3. CoroutineScope.launch:推荐方式(结构化并发)
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
    // 协程逻辑
}
scope.cancel() // 取消所有子协程

2. 挂起函数(Suspend Functions)

在函数前添加关键字suspend,声明这个函数为挂起函数; 若我们希望两个挂起函数顺序调用,在协程中,按照正常的顺序书写即可,后面的挂起函数可以正常获取前面的挂起函数,即两者是顺序执行的

suspend fun fetchData(): String {
    delay(1000) // 模拟网络请求
    return "Data"
}

// 在协程中调用挂起函数
launch {
    val data = fetchData()
    println(data)
}

3. 异步任务与结果获取

若两个挂起函数之间没有依赖,我希望它们能并行调度,方便我快速获取结果,此时可以借助 async 来实现

在概念上,async 就类似于 launch。它启动了一个单独的协程,这是一个轻量级的线程并与其它所有的协程一起并发的工作。不同之处在于 launch 返回一个 Job 并且不附带任何结果值,而 async 返回一个 Deferred —— 一个轻量级的非阻塞 future, 这代表了一个将会在稍后提供结果的 promise。你可以使用 .await() 在一个延期的值上得到它的最终结果, 但是 Deferred 也是一个 Job,所以如果需要的话,你可以取消它。

suspend fun main() = coroutineScope {
    val deferred1 = async { loadData1() } // 启动异步任务
    val deferred2 = async { loadData2() }
    val result = deferred1.await() + deferred2.await() // 获取结果
}

四、协程的核心组件

1. 协程作用域(CoroutineScope)

管理协程的生命周期,确保资源正确释放:

class MyViewModel : ViewModel() {
    // 使用 viewModelScope(Android 架构组件提供)
    fun fetchData() = viewModelScope.launch {
        // 协程逻辑
    }
}

在 Android 开发过程中,我们需要理解一些协程代码运行的范围。而所有的Scope 如 GlobalScope 都是 CoroutineScope 的子类,我们的协程创建都需要这样一个 CoroutineScope 来启动。

一些常见的作用域 CoroutineScope 对象。

  • GlobeScope:全局范围,不会自动结束执行。
  • MainScope:主线程的作用域,全局范围
  • lifecycleScope:生命周期范围,用于activity等有生命周期的组件,在DESTROYED的时候会自动结束。
  • viewModelScope:viewModel范围,用于ViewModel中,在ViewModel被回收时会自动结束

手动创建一个作用域

val scope = CoroutineScope(Dispatchers.IO)

2. 协程调度器(Dispatchers)

指定协程执行的线程池:

  • Dispatchers.Main:主线程(UI 线程),用于更新 UI。
  • Dispatchers.IO:适合 I/O 密集型任务(默认 64 线程)。
  • Dispatchers.Default:适合 CPU 密集型任务(默认线程数为 CPU 核心数)。
  • newSingleThreadContext:创建专用单线程。

3. Job 与协程生命周期

val job = launch {
    // 协程体
}

job.start()   // 启动协程
job.cancel()  // 取消协程
job.join()    // 等待协程完成

五、协程的工作原理

  1. 挂起与恢复

    • 协程通过状态机实现挂起,将局部变量保存在对象中。
    • 挂起时释放线程,恢复时从上次暂停处继续执行。
  2. Continuation Passing Style (CPS)

    • 编译器将 suspend 函数转换为带 Continuation 参数的状态机。

    • 示例:

      // 原始代码
      suspend fun main() {
          println("Before")
          delay(1000)
          println("After")
      }
      
      // 编译后(简化)
      fun main(continuation: Continuation<Unit>): Any {
          when (continuation.label) {
              0 -> {
                  println("Before")
                  return delay(1000, continuation.copy(label = 1))
              }
              1 -> {
                  println("After")
                  return Unit
              }
          }
      }
      
  3. 调度器工作流程

    • 协程调度器管理线程池,将协程任务分发给空闲线程。
    • 非阻塞操作(如 delay)通过回调机制恢复执行。

六、协程的异常处理

1. 结构化异常处理

// 使用 try-catch
launch {
    try {
        fetchData()
    } catch (e: Exception) {
        // 处理异常
    }
}

// 使用 supervisorScope 隔离子协程异常
supervisorScope {
    launch { /* 可能抛出异常的协程 */ }
    launch { /* 不受影响的协程 */ }
}

2. 全局异常处理器

val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
    println("Caught exception: $throwable")
}

// 应用于协程作用域
val scope = CoroutineScope(Dispatchers.IO + exceptionHandler)

七、协程的最佳实践

1. 避免使用 GlobalScope

// 错误做法
GlobalScope.launch { /* ... */ }

// 正确做法
class MyClass {
    private val scope = CoroutineScope(Dispatchers.IO + Job())
    
    fun doWork() = scope.launch { /* ... */ }
    
    fun cleanup() = scope.cancel() // 生命周期结束时取消
}

2. 选择合适的调度器

// UI 操作在主线程
launch(Dispatchers.Main) {
    textView.text = "Loading..."
    val data = withContext(Dispatchers.IO) { fetchData() } // 切换到 IO 线程
    textView.text = data
}

3. 避免协程嵌套

// 错误做法:嵌套协程
launch {
    launch { /* 子协程 */ }
}

// 正确做法:使用 async 组合结果
val result = coroutineScope {
    val deferred1 = async { task1() }
    val deferred2 = async { task2() }
    deferred1.await() + deferred2.await()
}

4. 处理背压(Backpressure)

当生产者速度快于消费者时,使用 ChannelFlow

// 使用 buffer 处理背压
flow {
    for (i in 1..1000) emit(i)
}.buffer(100) // 缓冲 100 个元素
    .collect { /* 处理元素 */ }

5. 资源管理

// 使用 use 自动关闭资源
withContext(Dispatchers.IO) {
    File("data.txt").useLines { lines ->
        lines.forEach { println(it) }
    }
}

八、协程的性能优化

  1. 减少协程创建开销

    • 避免在循环中创建大量协程,使用 map + awaitAll 批量处理。
    val results = list.map { async { process(it) } }.awaitAll()
    
  2. 复用调度器

    • 避免频繁创建新的 SingleThreadContext,使用共享实例。
  3. 监控协程泄漏

    • 使用 DebugProbes(测试环境)检测未完成的协程。
    // 在测试中使用
    DebugProbes.install()
    // 测试结束后检查
    DebugProbes.assertNoActiveCoroutines()
    

九、常见陷阱与注意事项

  1. 阻塞 vs 挂起

    • 避免在协程中使用 Thread.sleep()(阻塞线程),应使用 delay()(挂起协程)。
  2. 线程安全

    • 协程不保证线程安全,共享可变状态时需同步(如使用 Mutex)。
  3. 内存泄漏

    • 长生命周期协程引用短生命周期对象(如 Activity)时需谨慎。
  4. 测试协程代码

    • 使用 TestCoroutineDispatcher 控制协程执行:
    @Test
    fun testCoroutine() = runBlockingTest {
        // 测试协程逻辑
    }
    

十、协程的应用场景

  1. 异步 I/O 操作

    • 网络请求、文件读写等。
  2. UI 响应性优化

    • 将耗时操作放在后台协程,保持 UI 流畅。
  3. 并发任务处理

    • 并行执行多个独立任务,合并结果。
  4. 数据流处理

    • 使用 Flow 处理异步数据流。
  5. 状态机实现

    • 通过协程实现复杂的状态流转逻辑。

总结

Kotlin 协程通过轻量级、非阻塞的特性,彻底改变了异步编程的体验。其核心优势在于:

  • 高效资源利用:减少线程创建开销,提升系统吞吐量。
  • 简洁代码结构:使用同步写法实现异步逻辑,避免回调地狱。
  • 安全的并发模型:通过结构化并发和作用域管理,降低内存泄漏风险。

掌握协程需要理解其核心概念(作用域、调度器、挂起函数)和最佳实践,避免常见陷阱。在实际项目中,协程特别适合处理 I/O 密集型任务和需要高响应性的应用场景。

Loading...