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
什么时候用协程
当你需要切线程或者指定线程的时候。你要在后台执行任务
好处:有回调,像同步的方式写异步代码
- 并发实现方便
- 没有回调嵌套发生, 代码结构清晰
suspend
挂起函数在执行完成之后,协程会重新切回它原先的线程。
挂起,挂起函数在执行完成之后一个稍后会被自动切回来的线程调度操作。
代码执行到 suspend 函数的时候会『挂起』,并且这个『挂起』是非阻塞式的,它不会阻塞你当前的线程
「切回来」就类似于协程会帮我再 post 一个 Runnable,让我剩下的代码继续回到主线程去执行。
这个函数实质上并没有发生挂起,那你这个 suspend 关键字只有一个效果:就是限制这个函数只能在协程里被调用,如果在非协程的代码中调用,就会编译不通过。
正确用法:给函数加上 suspend 关键字,然后在 withContext 把函数的内容包住就可以了。
示例
1 | |
await
先用 async 启动任务 → await () 等待结果
await() 不会阻塞主线程,只是会挂起协程,当requestData()执行完返回结果后,processData()的逻辑会恢复执行
1 | |
withContext
这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行
withContext 会阻塞上下文线程
1 | |
- **
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
6val job = launch(start = CoroutineStart.ATOMIC) {
println("前半段一定会执行,直到第一个挂起点")
delay(1000) // 这里开始才可以被取消
println("挂起后才可以被取消")
}
🔹 4. UNDISPATCHED
- 行为:先在当前线程执行,遇到挂起点后再切换到指定调度器。
- 特点:
- 避免不必要的线程切换。
- 常用于启动时需要立即运行的任务。
1
2
3
4
5
6
7
8
9
10
11
12fun 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 | 当前线程立即执行到第一个挂起点,再切换调度器 | 避免线程切换开销 |
串行并行

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
12launch {
supervisorScope {
val task = async {
methodThatThrowsException()
}
try {
updateUI("Ok ${task.await()}")
} catch (e: Throwable) {
showError("Erro! ${e.message}")
}
}
}
Coroutine(协程)
Coroutine Scope /ˈkəʊruːˌtiːn/
CoroutineScope是协程作用域,其内部本身就含有一个CoroutineContext线程,默认是主线程。
而CoroutineContext则是在协程作用域中执行的线程切换。
CoroutineScope 会跟踪它使用 launch 或 async 创建的所有协程。您可以随时调用 scope.cancel() 以取消正在进行的工作(即正在运行的协程)。
viewModelScope
1 | |
Dispatchers
- Dispatchers.Main:Android 中的主线程
- Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求
- Dispatchers.Default:适合 CPU 密集型的任务,比如计算
原理
我们要保证不阻塞,同时又要是同步式写法,该怎样做呢?异步回调callback是一种不阻塞的方式,底层也是这种实现方式,只不过外层帮我们封装成现在的同步式写法了。
1 | |
1 | |
- 用
suspend关键字标记 - 编译器会将其转换为状态机
- 可以在不阻塞线程的情况下暂停执行
- 每个挂起点都是一个状态机状态
callback.resume (posts)
**resume(value) → 异步任务完成时恢复协程,并传回结果。
普通接口回调改成协程
在 await 函数的挂起结束以后,它会调用 continuation 参数的 resumeWith 函数,来恢复执行 await 函数后面的代码。


