2.Kotlin 协程总结

协程概念

协程就是一个线程。
协程原理:
https://juejin.cn/post/7212311942613385253#heading-15 
速通协程,一步到位!

协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。

协程很重要的一点就是当它挂起的时候,它不会阻塞其他线程。协程底层库也是异步处理阻塞任务,但是这些复杂的操作被底层库封装起来,协程代码的程序流是顺序的,不再需要一堆的回调函数,就像同步代码一样,也便于理解、调试和开发。它是可控的,线程的执行和结束是由操作系统调度的,而协程可以手动控制它的执行和结束。其实就是协程自己加了回调

协程可以理解为协程各自在各自的线程上,且线程不同。其实如果多个协程共用一个线程,其实它们之间也就没有线程并发问题了

为什么需要 Kotlin 协程

提供方便的线程操作API,编写逻辑清晰且简洁的线程代码。
协程是Google在 Android 上进行异步编程的推荐解决方案。具有如下特点:

轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。

内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。
Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发

好文章:https://blog.csdn.net/poorkick/article/details/112131961?spm=1001.2014.3001.5501

什么时候用协程

当你需要切线程或者指定线程的时候。你要在后台执行任务
好处:有回调,像同步的方式写异步代码

  1. 并发实现方便
  2. 没有回调嵌套发生, 代码结构清晰

suspend

挂起函数在执行完成之后,协程会重新切回它原先的线程
挂起,挂起函数在执行完成之后一个稍后会被自动切回来的线程调度操作。

代码执行到 suspend 函数的时候会『挂起』,并且这个『挂起』是非阻塞式的,它不会阻塞你当前的线程
「切回来」就类似于协程会帮我再 post 一个 Runnable,让我剩下的代码继续回到主线程去执行。
这个函数实质上并没有发生挂起,那你这个 suspend 关键字只有一个效果:就是限制这个函数只能在协程里被调用,如果在非协程的代码中调用,就会编译不通过。

正确用法:给函数加上 suspend 关键字,然后在 withContext 把函数的内容包住就可以了。

示例

1
2
3
4
5
6
GlobalScope.launch(Dispatchers.Main) {
  val image = suspendingGetImage(imageId)  // 获取图片
  avatarIv.setImageBitmap(image)           // 显示出来
}

suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) { ...}

await

先用 async 启动任务 → await () 等待结果
await() 不会阻塞主线程,只是会挂起协程,当requestData()执行完返回结果后,processData()的逻辑会恢复执行

1
2
3
4
5
6
7
val deferred = async(Dispatchers.IO) {
// 子线程执行
fetchDataFromNetwork()
}

val result = deferred.await()
val posts = postsDeferred.await()

withContext

这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行
withContext 会阻塞上下文线程

1
2
3
4
5
6
val user = withContext(Dispatchers.IO) { 
fetchUser() // 在 IO 线程执行
}
// 自动切回调用者的线程(比如 Main)
println(user)
val posts = withContext(Dispatchers.IO) { fetchPosts() }
  • **await**:多个任务并发执行,时间线里任务是平行的,等到结果再继续 → 高效
  • withContext:切换到指定线程池,但代码是串行执行,一个任务完成再跑下一个 → 更适合需要顺序和上下文切换的场景

二种启动

  • launch 可启动新协程而不将结果返回给调用方。任何被视为“一劳永逸”的工作都可以使用 launch 来启动。
  • async 会启动一个新的协程,并允许您使用一个名为 await 的暂停函数返回结果。

启动模式

在 Kotlin 协程里,launch / async 等构建器有一个参数 start,它决定了 协程的启动模式
共有 4 种启动模式CoroutineStart 枚举):

🔹 1. DEFAULT(默认)

  • 行为:立即调度协程执行(如果调度器允许的话),但在第一个挂起点之前都是同步执行的。
  • 特点
    • 如果协程体里没有挂起点,可能会在当前线程立即执行。
    • 是最常用的模式。

val job = launch(start = CoroutineStart.DEFAULT) { println("协程开始执行") }

🔹 2. LAZY

  • 行为:协程不会自动执行,只有在以下情况才会启动:
    • 调用 start()
    • 调用 join()
    • 调用 await()(对于 async 协程)
  • 特点:适合需要“按需执行”的任务。

val job = launch(start = CoroutineStart.LAZY) { println("只有调用 job.start()/join() 时才会执行") } job.start() // 手动启动

🔹 3. ATOMIC

  • 行为:协程会立即执行,直到遇到第一个挂起点之前 不可被取消
  • 特点:保证了协程至少能跑到第一个挂起点,不会在一开始就被取消掉。
    1
    2
    3
    4
    5
    6
    val job = launch(start = CoroutineStart.ATOMIC) {
    println("前半段一定会执行,直到第一个挂起点")
    delay(1000) // 这里开始才可以被取消
    println("挂起后才可以被取消")
    }

🔹 4. UNDISPATCHED

  • 行为:先在当前线程执行,遇到挂起点后再切换到指定调度器。
  • 特点
    • 避免不必要的线程切换。
    • 常用于启动时需要立即运行的任务。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      fun main() = runBlocking {
      println("main thread = ${Thread.currentThread().name}")

      launch(Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) {
      println("协程启动,线程 = ${Thread.currentThread().name}")
      delay(1000) // 挂起点
      println("恢复后,线程 = ${Thread.currentThread().name}")
      }

      println("主线程继续执行")
      }

main thread = main
协程启动,线程 = main
主线程继续执行
恢复后,线程 = DefaultDispatcher-worker-1

🔹 总结对比

启动模式 行为 适用场景
DEFAULT 立即调度执行(第一个挂起前可能同步执行) 默认推荐
LAZY 只有调用 start()/join()/await() 时才执行 按需启动
ATOMIC 立即执行,直到第一个挂起点前不可取消 保证起始逻辑一定执行
UNDISPATCHED 当前线程立即执行到第一个挂起点,再切换调度器 避免线程切换开销

串行并行

image.png|600

Job

Job 是协程的句柄。使用 launch 或 async 创建的每个协程都会返回一个 Job 实例,该实例唯一标识协程并管理其生命周期。您还可以将 Job 传递给 CoroutineScope 以进一步管理其生命周期 (关闭)

SupervisorJob

用 SupervisorJob 替代 Job,SupervisorJob 与 Job 基本类似,区别在于不会被子协程的异常所影响

处理协程异常

如何优雅的处理协程的异常?
不会影响其他,SupervisorJob 让协程自己处理异常。它不会将子协程的异常向上传播。不会影响到其他子协程,也不会导致父协程的取消,可以在子协程内部使用 try/catch 来捕获异常。

Job 会传播异常,所以 catch 代码块不会被调用

  • 当一个子协程抛出异常时,这个异常会向上传播到父协程。
  • 父协程会因此被取消(cancel),然后它再取消所有的其他子协程。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    launch {
        supervisorScope {
            val task = async {
                methodThatThrowsException()
            }
            try {
                updateUI("Ok ${task.await()}")
            } catch (e: Throwable) {
                showError("Erro! ${e.message}")
            }
        }
    }
    image.png|800|600

Coroutine(协程)

Coroutine Scope    /ˈkəʊruːˌtiːn/ 
CoroutineScope是协程作用域,其内部本身就含有一个CoroutineContext线程,默认是主线程

而CoroutineContext则是在协程作用域中执行的线程切换。
CoroutineScope 会跟踪它使用 launch 或 async 创建的所有协程。您可以随时调用 scope.cancel() 以取消正在进行的工作(即正在运行的协程)。

viewModelScope

1
2
3
   private val viewModelScope: CoroutineScope by lazy {
        CoroutineScope(SupervisorJob() + Dispatchers.Main)
    }

Dispatchers

  • Dispatchers.Main:Android 中的主线程
  • Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求
  • Dispatchers.Default:适合 CPU 密集型的任务,比如计算

原理

我们要保证不阻塞,同时又要是同步式写法,该怎样做呢?异步回调callback是一种不阻塞的方式,底层也是这种实现方式,只不过外层帮我们封装成现在的同步式写法了。

1
2
3
4
5
6
// 看起来是同步的代码
suspend fun fetchUserData() {
val user = api.getUser() // 看似阻塞的同步调用
val posts = api.getPosts(user.id) // 另一个看似阻塞的调用
showData(user, posts)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 近似底层实现(概念性伪代码)
fun fetchUserData(callback: Continuation) {
// 状态机实现
when(callback.label) {
0 -> {
// 发起异步请求
api.getUserAsync { user ->
// 请求完成后恢复协程
callback.resume(user)
}
callback.label = 1 //把 `label` 改为 `1`,表示下次从状态 1 开始执行。
}
1 -> {
//从上一步拿到 `user`。调用 `api.getPostsAsync(user.id)` 获取用户的帖子。
val user = callback.result as User
api.getPostsAsync(user.id) { posts ->
callback.resume(posts)
}
callback.label = 2
}
2 -> {
val posts = callback.result as List<Post>
showData(user, posts)
}
}
}
  • 用 suspend 关键字标记
  • 编译器会将其转换为状态机
  • 可以在不阻塞线程的情况下暂停执行
  • 每个挂起点都是一个状态机状态

callback.resume (posts)
​**​resume(value) → 异步任务完成时恢复协程,并传回结果。

普通接口回调改成协程

在 await 函数的挂起结束以后,它会调用 continuation 参数的 resumeWith 函数,来恢复执行 await 函数后面的代码。

image.png|600
image.png|600
image.png|600


2.Kotlin 协程总结
http://peiniwan.github.io/2025/12/7c96486907e7.html
作者
六月的雨
发布于
2025年12月16日
许可协议