1、什么是协程?和线程有什么区别
答案
协程是轻量级线程,基于线程封装,由语言层调度,不是操作系统调度。
区别:
- 线程由 OS 调度、开销大;协程开销极小、可开上万个。
- 线程是抢占式;协程是协作式,主动挂起、恢复。
- 协程可非阻塞挂起,不阻塞当前线程。
- 协程自带上下文、作用域、生命周期,更适合 Android 异步开发。
2、协程的优点
答案
- 轻量、占用资源少,支持大量并发;
- 用同步写法写异步代码,告别嵌套回调;
- 自带作用域,可统一取消、防泄漏;
- 灵活切换调度线程(主线程 / 子线程);
- 配合 Flow、Jetpack 生态适配更好。
3、什么是挂起函数 suspend?作用是什么
答案
suspend 修饰的函数是挂起函数;
特点:
- 只能在协程或其他挂起函数中调用;
- 不会阻塞线程,只是挂起当前协程,让出线程执行其他任务;
- 耗时结束后自动恢复继续执行。
4、suspend 工作原理
答案
suspend 函数编译后携带续体对象,
执行时保存运行状态实现挂起,不阻塞线程;
异步任务完成后通过续体恢复现场继续执行,从而实现同步写法、异步执行。
加分短句
挂起阻塞协程,不阻塞线程;靠续体完成状态保存与恢复,无回调嵌套。
5、协程三大核心:协程作用域、调度器、生命周期
答案
- 作用域 Scope:管理协程生命周期,统一取消、控制启停;
- 调度器 Dispatcher:指定协程跑在哪个线程;
- Job:协程任务句柄,可取消、监听完成状态。
6、四大调度器 Dispatcher 区别
答案
- Dispatcher.Main:主线程,更新 UI;
- Dispatcher.IO:子线程,适合网络、文件、数据库 IO;
- Dispatcher.Default:子线程,适合CPU 密集型复杂计算;
- Dispatcher.Unconfined:不指定线程,随上下文执行,几乎不用。
7、常用协程作用域有哪些
答案
| 方式 | 用途 | 推荐等级 |
|---|---|---|
GlobalScope | 全局协程(内存泄漏) | ❌ 不推荐 |
runBlocking | 测试 / 阻塞主线程 | ⚠️ 仅测试 |
lifecycleScope | Android 页面异步任务 | ✅ 顶级推荐 |
viewModelScope | ViewModel 异步任务 | ✅ 顶级推荐 |
CoroutineScope() | 自定义生命周期协程 | ✅ 通用推荐 |
launch | 无返回值异步任务 | ✅ 最常用 |
async + await | 并发任务 + 获取返回值 | ✅ 常用 |
withContext | 线程切换(IO / 主线程) | ✅ 必用 |
8、为什么不推荐用 GlobalScope
答案
GlobalScope 生命周期贯穿整个 App,页面退出无法自动取消;里面的网络、延时任务还在后台跑,极易造成内存泄漏、浪费资源。
9、launch 和 async 区别
答案
-
launch:无返回值,返回Job,只管执行 -
async:有返回值,返回Deferred,通过await()拿结果,适合并发任务
10、await () 作用
答案
等待 async 协程执行完成,挂起当前协程,拿到返回结果;不会阻塞线程,只是协程挂起。
11、协程怎么取消?取消原理
答案
通过 Job.cancel () 取消协程;
协程在挂起点会响应取消,抛出 CancellationException;作用域取消后,内部所有子协程全部级联取消。
12、协程异常怎么处理?CoroutineExceptionHandler
答案
- 单个协程内部用 try-catch 包裹挂起函数;
- 全局统一异常用 CoroutineExceptionHandler;
- launch 默认异常会崩溃;async 异常在 await 时才抛出。
13、协程结构化并发
结构化并发:子协程依附父协程作用域,父子生命周期绑定,自动取消、自动异常传递,杜绝协程泄漏。
核心三点
- 父子协程关系协程有父子层级,子协程继承父Scope、Dispatcher、异常策略。
- 结构化取消父作用域一取消,所有子协程跟着全部级联取消,不用手动一个个解绑,天然防内存泄漏。
- 异常自动传播子协程抛异常,会向上传给父协程,统一捕获处理,不会静默崩溃、丢异常。
14、协程的启动模式几种
CoroutineStart 一共四种:DEFAULT、LAZY、ATOMIC、UNDISPATCHED
1、DEFAULT(默认)
立刻根据调度器调度执行,排队进入对应线程池,标准常规启动。
2、LAZY(懒启动)
不会自动执行,需要手动调用 start() / join() 才开始跑;适合需要延迟触发、手动控制时机的场景。
3、ATOMIC
立即在当前线程原子式启动,不可取消;在挂起之前,无法被取消,执行到第一个挂起点才响应取消。
4、UNDISPATCHED
直接在当前线程立刻执行,不切调度器;直到遇到第一个 suspend 挂起点,之后才按 Dispatcher 调度切换线程。
15、 let、also、with、run、apply 区别
| 函数名 | 使用方法 | 返回值 | 内部持有对象 | 作用 |
|---|---|---|---|---|
| apply | info.apply{this} | 本身 | this | 对象初始化、赋值属性 |
| also | info.also{it} | 本身 | it | 额外操作、日志、副作用 |
| let | info.let{it} | 最后一行 | it | 空安全调用、对象转换 |
| run | info.run{this} | 最后一行 | this | 计算、逻辑处理、返回结果 |
| with | with(info) {this} | 最后一行 | this | 对一个对象做多次操作 |
1. let
- 作用:空安全调用、对象转换
- 关键字:it(指代当前对象)
- 返回:最后一行
- 场景:
?.let判空处理、数据转换
2. also
- 作用:额外操作、日志、副作用
- 关键字:it
- 返回:自身对象
- 场景:链式调用中加日志、初始化后处理
3. apply
- 作用:对象初始化、赋值属性
- 关键字:无,直接用成员
- 返回:自身对象
- 场景:View、Intent、Bean 赋值
4. run
- 作用:计算、逻辑处理、返回结果
- 关键字:无
- 返回:最后一行
- 场景:复杂计算、多步逻辑
5. with
- 作用:对一个对象做多次操作
- 关键字:无
- 返回:最后一行
- 场景:非空对象,简化调用
6.最核心记忆口诀(面试必背)
- 初始化属性用 apply
- 空安全处理用 let
- 附加操作日志用 also
- 计算返回值用 run
- 非空简化调用用 with
7、记忆口诀:
- “让它(let)转换,也(also)记录,运行(run)计算,应用(apply)配置,与(with)共事。”
- 返回自身是
also和apply(想记“AA”原则:Also Apply 都返回自己)。 - 使用
it的是also和let。
16、Kotlin 协程怎么处理异常的?
-
两种传播方式:
launch构建器:异常会立即在发生处抛出,并开始传播。async构建器:异常会延迟到您调用.await()获取结果时才抛出。
-
方式一:直接
try-catch(最基础)
直接在可能发生异常的代码块周围使用 try-catch。这对于 launch 和 async 都有效。
scope.launch {
try {
fetchData() // 挂起函数,可能抛出异常
} catch (e: Exception) {
// 处理异常,例如更新UI显示错误
showError(e.message)
}
}
// 对于 async,必须在 await() 处捕获
scope.launch {
val deferred = async { fetchData() }
try {
val result = deferred.await()
process(result)
} catch (e: Exception) {
// 处理来自 async 块的异常
}
}
3. 方式二:CoroutineExceptionHandler(全局捕获)
这是一个上下文元素,用于在协程的根节点(通常是顶级或 SupervisorJob 下的直接子协程)捕获未处理的异常。它就像是协程世界的“全局 Thread.uncaughtExceptionHandler”。
// 1. 定义异常处理器
val handler = CoroutineExceptionHandler { _, exception ->
println("协程上下文捕获到未处理异常: $exception")
// 可以在这里上报崩溃日志
}
// 2. 在作用域中使用(注意:必须在根协程或 SupervisorJob 的直接子协程才有效)
val scope = CoroutineScope(Job() + handler) // 作为根作用域的一部分
scope.launch {
// 如果这里发生异常,会被 handler 捕获
throw RuntimeException("测试异常")
}
// 以下方式无效!异常会直接崩溃,因为handler不是根协程的
scope.launch {
launch(handler) { // ❌ 错误:handler 不是这个 launch 的根
throw RuntimeException("这个异常 handler 抓不到!")
}
}
4. 方式三:SupervisorJob / supervisorScope(隔离异常)
这是防止“一个失败,全家遭殃” 的关键。默认情况下,父协程使用普通 Job,一个子协程失败会取消所有兄弟协程。SupervisorJob 改变了这一规则,使子协程的失败彼此隔离。
// 1. 使用 SupervisorJob 创建作用域
val supervisorScope = CoroutineScope(SupervisorJob())
supervisorScope.launch {
delay(1000)
throw RuntimeException("子协程1失败") // 这个会崩溃或由handler处理
}
supervisorScope.launch {
delay(2000)
println("子协程2仍然会执行!") // 尽管协程1失败了,但协程2不受影响
}
// 2. 使用 supervisorScope 构建器(更常用)
suspend fun handleMultipleTasks() = supervisorScope { // 进入一个监督作用域
val child1 = launch {
throw RuntimeException("任务A失败")
}
val child2 = launch {
delay(500)
println("任务B正常完成") // 任务A的失败不会取消任务B
}
// 可以等待所有子协程,并单独处理它们的失败
child1.join()
child2.join()
}
5. 方式四:在 async 块内部捕获并返回结果状态
这是处理 async 异常的推荐模式,可以避免在 await() 时抛出异常,而是返回一个包含成功/失败信息的结果类。
// 定义结果密封类
sealed class Result<out T> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Throwable) : Result<Nothing>()
}
suspend fun fetchDataSafely(): Result<Data> = withContext(Dispatchers.IO) {
return@withContext try {
Result.Success(apiService.getData()) // 假设 getData() 是 suspend fun
} catch (e: Exception) {
Result.Error(e)
}
}
// 在调用处,无需 try-catch await()
scope.launch {
val deferred = async { fetchDataSafely() }
when (val result = deferred.await()) { // 这里不会抛出异常
is Result.Success -> updateUI(result.data)
is Result.Error -> showError(result.exception)
}
}
17、launch为什么不用 join 也会执行?
launch 启动的协程,不用调用 join () 也会自动执行!
因为:
- launch = 启动一个新的协程并自动执行
- join () = 等待这个协程执行完
它们的关系是:
- launch 负责 “启动并跑”
- join 负责 “等它跑完”
val job = launch {
delay(1000)
println("任务完成")
}
job.join() // 等待协程执行完
println("全部结束")
结果:
//不加job.join()
全部结束
任务完成
//加job.join()
任务完成
全部结束
最核心总结
-
launch 会自动执行协程,不需要 join
-
join () 只做一件事:等待协程执行完毕,会阻塞尽量不要使用
-
不加 join → 不等待,继续往下跑
-
加了 join → 停下来等协程跑完再继续
一句话满分背诵版
launch 启动协程后会自动执行,join () 不是启动协程的,只是用来等待协程结束。
18、Kotlin 协程:到底是怎么 “挂载 / 切换” 到别的线程的?
一句话结论
协程本身不创建线程,它是靠 Dispatcher(调度器) 把协程体(你写的代码) 提交给线程池 / 主线程去执行,从而实现 “挂载到别的线程”。
你可以把协程理解成:一段可以被挂起、可以在线程之间搬运的代码块。
1. 关键角色:Dispatcher(调度器)
它就是协程的 “线程司机”,负责决定:
- 这段协程代码去哪个线程执行
- 线程从哪里来(线程池)
Kotlin 自带 4 个常用调度器:
- Dispatchers.Main → 安卓主线程(UI 线程)
- Dispatchers.IO → IO 线程池(网络、数据库)
- Dispatchers.Default → CPU 密集型线程池
- Dispatchers.Unconfined → 不指定线程(几乎不用)
2. 协程 “挂载到别的线程” 的完整流程
我用最常用的 withContext 给你走一遍流程,一看就懂:
kotlin
scope.launch(Dispatchers.Main) {
// 1. 现在:运行在 主线程
// 2. 执行 withContext,协程挂起 ↓
val result = withContext(Dispatchers.IO) {
// 3. 现在:协程被“挂载”到 IO 线程池的某个线程
api.request() // 耗时操作
}
// 4. 挂起恢复,自动切回 主线程
updateUI(result)
}
3.底层发生了什么?(超清晰)
-
当前线程执行到
withContext协程发现要切换调度器,于是挂起,不阻塞当前线程。 -
Dispatcher 把协程体提交给目标线程池
Dispatchers.IO内部是一个高效线程池,它会:- 从池里拿一条空闲线程
- 把你写的
{ ... }代码块丢给这条线程执行
-
协程在新线程上恢复运行代码块运行时,当前线程已经变了,这就是 “挂载到别的线程”。
-
执行完,自动切回原来的线程协程有状态记录,执行完自动回到之前的调度器(主线程)。
4. 最容易踩的坑
- 不指定 Dispatcher 默认继承外层协程的线程
- 不要在主线程做耗时操作
- Dispatchers.Main 必须添加依赖(Android)
5. 终极总结
- 协程本身不创建线程,靠
Dispatcher切换线程 - 挂载 = 把协程代码块提交到目标线程池执行
- withContext 是最标准、最安全的线程切换方式
19、所有切换线程的方式
协程切换线程只有 2 种正确用法:
① launch /async 直接指定(启动就去目标线程)
kotlin
scope.launch(Dispatchers.IO) {
// 一启动就在 IO 线程
}
② withContext 中途切换(最常用、官方推荐)
kotlin
withContext(Dispatchers.IO) {
// 切换到 IO 执行
}
20、 withContext 作用?和 async/await 区别?
答:
withContext:切换调度器、切换线程,挂起函数,直接返回结果(最后一行的结果);
不需要手动 await,代码更简洁,推荐优先用。
区别:
- withContext 是串行切换(会阻塞下面的执行);
- async 适合并发多任务。
21、withContext 内部是串行还是并发?
一句话结论
withContext 内部代码块是串行执行,不是并发;它只是切换线程 / 调度器,不开启新协程。
详细解释(面试可直接说)
withContext只是一个挂起函数,作用是切调度器、切线程,不会新建协程。- 大括号里的代码,从上到下顺序串行执行,一行走完才走下一行,没有并发。
- 如果想并发,必须手动用
async + await开启多个子协程。
代码直观对比
1)withContext 串行(顺序执行)
kotlin
lifecycleScope.launch {
println("开始")
withContext(Dispatchers.IO) {
delay(3000)
println("withContext 内部")
}
println("必须等withContext完才走这里")
}
withContext 不新建协程(同一个协程内是串行),必须等
2)async 才是并发
kotlin
launch {
val d1 = async(Dispatchers.IO) { task1() }
val d2 = async(Dispatchers.IO) { task2() }
// 两个任务同时并发跑
d1.await()
d2.await()
}
3)launch 也是并发
launch {} 一定会创建一个全新的子协程
1、核心结论
- launch:直接创建新协程,异步启动,不会阻塞 / 等待当前协程后续代码。
- withContext:不创建新协程,复用当前协程,只是切线程、挂起等待,后面代码要等它跑完。
2、代码对比一眼看懂
示例 1:launch 新建协程,不等待
kotlin
lifecycleScope.launch {
println("外层开始")
// 新开一个独立协程
launch(Dispatchers.IO) {
delay(3000)
println("内层 launch 执行完")
}
// 不会等上面 launch,直接立马执行
println("外层直接往下走,不等内层")
}
输出顺序:
plaintext
外层开始
外层直接往下走,不等内层
(3秒后)内层 launch 执行完
底层原理(面试加分)
withContext只会切换当前协程的调度环境,始终还是同一个协程,单协程里代码必然串行。async会创建新的子协程,多个协程交给线程池调度,才能实现并发。
面试精准口述版
- launch 一定会创建新的子协程,属于协程构建器;
- 新协程独立运行,不会阻塞当前父协程,后面代码立刻执行,不用等待;
- 而 withContext 不创建新协程,复用当前协程,只是切换调度器并挂起,后续代码必须等待它执行完毕。
22、withContext 的后面代码也会等待withContext 执行后,再执行
withContext 是挂起函数,后面的代码必须等 withContext 整块执行完、返回结果,才会往下走。
1. 核心本质
withContext 是 suspend 挂起函数:
- 执行到
withContext→ 协程挂起 - 内部代码全部跑完 → 恢复协程
- 再执行 withContext 后面的代码
2. 示例一看就懂
kotlin
lifecycleScope.launch {
println("1 开始")
// 会卡在这里等内部全部执行完
val res = withContext(Dispatchers.IO) {
delay(2000) // 模拟耗时
println("2 耗时任务结束")
"结果"
}
// 必须等上面 withContext 完事,才会走到这里
println("3 执行后面代码:$res")
}
执行顺序:1 开始 → 等 2 秒 → 2 耗时任务结束 → 3 执行后面代码
3. 为什么会等待?
withContext不是开新协程,还是当前同一个协程- 挂起函数的特性:调用处原地等待完成,不阻塞线程,但阻塞当前协程流程
- 天然是串行等待的效果
4. 对比:谁会等、谁不会等
- ✅ withContext:后面代码等待,串行
- ✅ delay:后面代码等待,串行
- ❌ launch 嵌套:不会等,直接往下走,并发
kotlin
launch {
launch(Dispatchers.IO) {
delay(2000)
println("子协程")
}
println("立马执行,不等上面")
}
面试一句话背记
withContext 是挂起函数,会阻塞当前协程流程,后面代码必须等它执行完毕才会继续执行;但它不会阻塞线程。
22、launch 内部是串行还是并发执行?
面试标准答案:launch 内部代码是串行,整体是并发
一句话秒杀
- launch 内部代码:从上到下 串行执行。
- launch 和外部 / 其他 launch:是 并发执行。
1. launch 内部的代码 → 串行
launch {} 大括号里面的代码,永远是顺序执行、一行一行跑,没有并发。
kotlin
scope.launch {
// 内部:串行!顺序执行!
task1()
task2() // 必须等 task1 完才执行
task3() // 必须等 task2 完才执行
}
原因:一个协程就是一个执行流,单执行流一定是串行的。
2. launch 和外部代码 → 并发
launch 会创建新协程,新协程和外部协程同时跑。
kotlin
scope.launch {
launch {
delay(1000)
println("A") // 并发执行
}
println("B") // 不会等上面,直接打印
}
输出顺序:
plaintext
B
(1秒后)A
原因:多个协程之间是并发的。
3. 多个 launch 之间 → 并发
kotlin
scope.launch {
launch { taskA() } // 并发
launch { taskB() } // 并发
launch { taskC() } // 并发
}
A、B、C 同时跑,谁先跑完不一定。
面试满分回答(背这段)
- launch 内部代码是串行执行,从上到下顺序执行;
- launch 会创建新协程,新协程与外部协程、以及其他 launch 之间是并发执行;
- 它不会等待内部执行完,外部代码会继续往下走。
终极对比(面试必考对比)
表格
| 方式 | 是否创建协程 | 内部执行 | 后面代码是否等待 |
|---|---|---|---|
| launch | ✅ 新建 | 串行 | 不等待(并发) |
| withContext | ❌ 不新建 | 串行 | 等待(串行) |
| async | ✅ 新建 | 串行 | 不等待(并发) |
23、 lifecycleScope /viewModelScope 原理?
答:
viewModelScope:绑定 ViewModel 生命周期,ViewModel 销毁自动取消协程lifecycleScope:绑定 Activity/Fragment 生命周期,页面销毁自动 cancel内部通过监听生命周期回调,自动取消协程,防泄漏。
24、 CoroutineExceptionHandler 什么时候生效?
答:
只对 根协程 生效,子协程异常会向上抛给根协程,由全局异常处理器捕获。
25、Flow 和 普通协程区别?
答: 协程单次返回一个结果(async+await);
Flow 是冷数据流,可以连续发送多个值,类似观察者模式,适合分页、实时数据、倒计时、接口轮询。
26、 协程挂起底层原理?
答:
Kotlin 编译器把 suspend 函数编译成状态机,拆分多个状态节点,挂起时保存上下文,恢复时从对应状态继续执行,不用阻塞线程。
27、runBlocking 用处?业务能用吗?
答:
runBlocking 会阻塞当前线程,
只用于单元测试、main 函数调试,业务代码禁止使用。
28、 协程会内存泄漏的场景有哪些?
答:
- 滥用 GlobalScope
- 协程内部持有 Activity/Fragment 引用
- 自定义 Scope 没手动 cancel
- 异步任务没取消,页面销毁还在回调
29、自定义协程(标准步骤)
- 组合 Dispatchers 调度器 + Job/SupervisorJob
- 手动构造
CoroutineScope - 用
launch/async在自定义作用域里开启协程 - 页面 / 业务销毁时 手动调用 cancel () 取消全部协程
// 自定义作用域
private val myScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// 开启自定义协程
fun requestData() {
myScope.launch {
// 耗时任务、网络请求
}
}
// 销毁时必须取消,防泄漏
fun destroy() {
myScope.cancel()
}
自定义注意事项(必背)
- 严禁用
GlobalScope替代自定义 - 必须自己手动 cancel,否则内存泄漏
- 优先用
SupervisorJob做异常隔离 - 自定义线程池一定要有界队列,防止 OOM
- 自定义 Dispatcher 用完要 close
- 作用域要和业务生命周期绑定
30、Kotlin协程为什么能以同步代码写出异步逻辑?
一句话核心
依靠挂起函数 + 状态机切线程,代码写法同步,底层执行异步。
精简完整版
- Kotlin 协程通过 suspend 挂起函数实现代码暂停与恢复,不会阻塞主线程。
- 编译期把挂起代码拆分成多个状态片段,自动生成状态机。
- 遇到耗时挂起函数自动切子线程执行,执行完毕自动切回原线程。
- 开发者只用顺序写同步风格代码,线程切换、回调拆分全部由编译器自动完成,摆脱回调地狱。
超短背诵版
协程利用挂起函数暂停任务,编译生成状态机自动分片执行,自动完成线程切换,上层写同步顺序代码,底层异步执行,所以能同步写法实现异步逻辑。
31、在协程上定义一个局部变量,为什么再其他线程里的协程也能访问到?
精简原理
- Kotlin 协程本质还是代码片段,局部变量会被闭包捕获,提升为类成员变量,不再是临时栈变量。
- 同一作用域下所有子协程,共用同一个作用域对象,变量引用完全一致。
- 协程只是切换线程执行代码,变量引用没变,所以不管切到哪个线程,都能读到同一个变量。
- 区别:只是执行线程变了,内存变量地址不变。
通俗大白话
你在父协程定义局部变量,协程编译时自动把这个变量包进闭包,所有子协程不管跑在主线程还是子线程,用的都是同一个内存地址变量,所以全都能访问。
面试加分提醒
- 能访问不等于线程安全,多协程同时修改会出现并发问题。
- 基本类型、引用类型都会被闭包持有,跨线程协程互通。
32、闭包什么意思?
一句话记住
函数内部,能拿到外面函数的局部变量,这个函数 + 外面的变量 = 闭包
大白话
- 外面方法定义了局部变量
- 里面的 Lambda / 匿名函数,抓走这个变量一直持有
- 哪怕外层代码走完销毁了,内部还能继续用这个变量这就叫闭包
协程场景最直白例子
kotlin
fun main() {
// 外层局部变量
var num = 0
// 协程就是内部函数,抓走了 num
GlobalScope.launch {
delay(1000)
num++ // 隔一秒还能访问外面的局部变量
println(num)
}
}
本来方法走完局部变量就销毁,被协程抓走持有了,一直活着,这就是闭包。
3 个核心特点(面试背)
- 内部函数访问外部局部变量
- 延长局部变量生命周期
- 多个内部代码共享同一个变量
最短背诵
闭包就是内层代码捕获外层局部变量,持有引用,延长变量生命周期,跨作用域依然能访问使用。
33、什么是Kotlin协程的状态机?
编译器把 suspend 挂起代码,拆分成多段代码片段,靠状态标记分段执行,这就是协程状态机。
- 你写同步顺序代码
kotlin
suspend fun test(){
println("1")
delay(1000)
println("2")
}
2. 编译后自动切成两段
- 状态 0:执行打印 1,遇到 delay暂停
- 状态 1:延迟结束,恢复执行打印 2
- 用数字状态值记录执行到哪一步,暂停存状态,恢复按状态继续跑,就是状态机
核心特点
- 不用手写回调,编译器自动拆分
- 挂起时保存当前状态,让出线程
- 恢复时读取状态,从断点继续执行
- 本质:分段执行 + 状态记录
面试必背短句
协程状态机是 Kotlin 编译器生成的,将连续挂起代码拆分成多个执行节点,通过状态值记录运行位置,实现暂停与恢复,从而做到同步写法异步执行。
34、在单个协程内部,delay是串行的,必须等delay之后,再执行后面的代码?
一句话
同一个协程内部,代码自上而下串行执行,delay 会挂起当前协程,必须等延时结束,才会往下走后面代码。
代码直观证明
kotlin
runBlocking {
println("开始")
delay(1000) // 挂起,等1秒
println("执行完第一个延迟")
delay(2000) // 再等2秒
println("全部结束")
}
执行顺序:开始 → 等 1 秒 → 打印 → 再等 2 秒 → 最后打印严格排队串行,不能并行
核心区分
- 同一协程内:delay 串行排队,依次等待执行
- 不同协程:互不影响,并发执行,互不等待
最简背诵
单个协程里代码顺序执行,delay 暂停当前协程,延时结束才执行下行代码,属于串行执行。