Skip to content

协程是 Kotlin 最大的亮点,也是 Android 开发里绕不开的东西。平时 launchasync 写得飞起,但"挂起"到底是什么意思、协程和线程什么关系、作用域为什么重要,这些背后的原理平时没细想。这篇把协程从概念到用法系统梳理一遍,把散落的知识点串成一条线。


一、协程是什么:轻量级线程

先纠正一个最常见的误解:协程不是线程,而是"跑在线程上的轻量级任务"

  • 一个线程同时只能跑一个任务,切换线程代价很大(上下文切换)。
  • 协程则是在一个线程上可以挂起、切换成千上万个,切换几乎零开销(用户态切换,不涉及系统调度)。

核心在于挂起(suspend)和阻塞(block)的区别

阻塞 block挂起 suspend
谁在等线程被卡住,啥也干不了协程让出线程,线程去干别的
资源线程资源被占用浪费线程被释放,可以跑别的协程
典型Thread.sleep()、同步 IOdelay()suspend 的 IO

一句话:阻塞浪费线程,挂起释放线程。这就是协程能"用少量线程扛大量并发"的根本原因——比如 1 个线程通过挂起,可以同时"等待" 10000 个网络请求,而不是开 10000 个线程。

二、挂起函数(suspend)

suspend 是协程的地基:

kotlin
suspend fun fetchUser(): User { ... }
  • 挂起函数可以被挂起、也能恢复,但它本身不阻塞当前线程
  • 挂起函数只能在协程里另一个挂起函数里调用(普通函数不能直接调)。
  • 调一个挂起函数,看起来是顺序执行,实际上内部遇到挂起点会让出线程,等结果回来了再接着往下跑——代码是"同步写法",执行是"异步效果"。

三、启动协程:launch / async / runBlocking

三个最常用的启动方式:

kotlin
// 1. launch:启动协程,返回 Job,不关心结果("发射后不管")
val job = launch {
    delay(1000)
    println("done")
}

// 2. async:启动协程,返回 Deferred,用 await 拿结果(要返回值)
val deferred = async { fetchUser() }
val user = deferred.await()

// 3. runBlocking:阻塞当前线程直到协程结束(一般只在 main / 测试用)
runBlocking {
    delay(1000)
}

记忆:launch 不返回值(Job),async 返回值(Deferred.await),runBlocking 会阻塞线程(别在业务里用)

四、作用域(CoroutineScope):为什么重要

作用域解决的是"协程的生命周期谁来管"。

kotlin
// 作用域里启动的协程,作用域取消时全部跟着取消
val scope = CoroutineScope(Dispatchers.Main)
scope.launch { ... }
scope.cancel()   // 所有子协程一起取消

几个要点:

  1. 结构化并发:协程在作用域里是"父子关系",父协程取消,子协程自动跟着取消——这是协程不会"泄漏"的关键。
  2. GlobalScope 慎用GlobalScope 是应用级作用域,生命周期和整个应用一样长,用它启动的协程不会被自动取消,容易内存泄漏,生产环境基本不用。
  3. Android 里的现成作用域lifecycleScope(跟随 Activity/Fragment 生命周期自动取消)、viewModelScope(跟随 ViewModel)。日常开发优先用这两个,而不是自己 new CoroutineScope

五、调度器(Dispatchers):协程跑在哪个线程

kotlin
launch(Dispatchers.IO) { /* 跑在 IO 线程池 */ }
launch(Dispatchers.Main) { /* 跑在主线程,更新 UI */ }
launch(Dispatchers.Default) { /* 跑在 CPU 密集线程池 */ }
调度器用途
Dispatchers.Main主线程,更新 UI
Dispatchers.IO网络、磁盘等 IO 操作
Dispatchers.DefaultCPU 密集计算

Android 的经典写法:IO 线程拿数据 → 切回 Main 更新 UI。

kotlin
lifecycleScope.launch {
    val user = withContext(Dispatchers.IO) { fetchUser() }  // IO 线程拿数据
    textView.text = user.name                               // 自动切回 Main
}

withContext 是"切线程"的常用工具——切到指定调度器执行一段代码,执行完自动切回来。

六、常见坑

  1. 滥用 GlobalScope:协程泄漏的头号原因。用 lifecycleScope / viewModelScope 替代。
  2. 异常会冒泡launch 里抛异常会让父协程一起挂(甚至崩溃),要 try-catch 包住协程体,或配 CoroutineExceptionHandlerasync 的异常在 await() 时才抛出。
  3. 取消是协作式的job.cancel() 只是打个标记,协程要自己检查取消状态(delaywithContext 这类挂起点会自动响应取消;但死循环里要自己 isActive 判断)。
  4. 别在挂起函数里用 Thread.sleep:它会真的阻塞线程,协程的优势就没了。用 delay()

小结

  • 协程 = 跑在线程上的轻量级任务,核心是挂起不阻塞线程
  • suspend 函数:可挂起恢复,同步写法异步执行
  • 启动三件套:launch(Job)、async(Deferred.await)、runBlocking(阻塞,测试用)
  • 作用域管生命周期:优先用 lifecycleScope / viewModelScope,别用 GlobalScope
  • 调度器:Main 更新 UI、IO 做 IO、Default 做计算,withContext 切线程
  • 坑:GlobalScope 泄漏、异常冒泡、取消是协作式、别用 Thread.sleep