Executor 和 CoroutinContenxt
一、Executor vs. CoroutineContext
-
Executor
- Java 的线程池接口,只负责将
Runnable提交到某个线程执行; - 不包含“主线程”概念,纯粹的线程调度器。
- Java 的线程池接口,只负责将
-
CoroutineContext
- Kotlin 协程的上下文,功能更强:不仅可以指定线程池(
CoroutineDispatcher),还可携带作业(Job)、名称(CoroutineName)等元素; launch { … }并非真正“新建线程”,而是将协程调度到CoroutineContext指定的线程或线程池执行。
- Kotlin 协程的上下文,功能更强:不仅可以指定线程池(
补充:在协程框架内部,负责线程切换的接口是
ContinuationInterceptor,其常见实现就是各种Dispatchers。Default,处理计算类型的调度器,没有什么IO,线程池大小一般就是与CPU的核心数量。
二、常见调度器(Dispatchers)
| 调度器 | 用途 | 线程池大小 |
|---|---|---|
Default | CPU 密集型任务(计算、排序、解析等),线程数≈CPU 核心数 | 与 CPU 核心数相当 |
IO | IO 密集型任务(磁盘读写、网络请求等),线程数较多以提升并发性能 | 默认 ≈64(可动态伸缩) |
Unconfined | 不强制切换线程:协程在当前调用点继续执行,直到首次挂起;挂起后恢复也不保证切回原线程 | — |
Main | Android 主线程,用于更新 UI | 主线程 |
Unconfined:不进行线程管理的一个分发器。写的代码在哪里就在那里执行,甚至是挂起行数里边的线程继续执行,不会有任何的切线程操作。
三、线程状态与 IO 操作(示意)
发起 IO 调用 IO 完成中断
| |
v v
Running → Blocked → Ready → Running
(发出系统调用) (线程休眠) (等待 CPU) (继续执行)
- IO 操作 发起后,线程进入 Blocked,不占用 CPU;IO 完成后由操作系统中断唤醒,再进入 Ready 等待调度。
四、挂起函数与协程“挂起/恢复”
-
挂起函数(如
withContext、delay):- 不会阻塞底层线程,而是挂起当前协程,释放线程给其它任务;
- 挂起发生时,协程保存现场,挂起函数内部可切换线程继续执行其它协程逻辑。
-
示例:线程切换与顺序执行
CoroutineScope(Dispatchers.Main).launch { // 切到 IO 线程执行网络请求 val data: String = withContext(Dispatchers.IO) { // 网络代码 "data" } // 切到 Default(CPU 线程池)执行数据处理 val processed: String = withContext(Dispatchers.Default) { // 处理代码 "processed $data" } println("Result: $processed") }withContext保证两段代码 串行 执行;- 若
withContext的调度器与当前相同,则不会真正切线程。
在Activity 或者Fragment中应该使用lifecyclerScope,因为这个是和页面的生命周期绑定的。如果是用ViewModel组件,最好使用ViewModelScope去开启协程。
五、协程启动与作用域
-
GlobalScopeGlobalScope.launch(Dispatchers.Main) { … }- 全局协程,不绑定任何生命周期,小心内存泄漏。
-
lifecycleScope(Activity/Fragment)lifecycleScope.launch { … }- 与页面生命周期绑定,页面销毁时自动取消协程;
- 默认派发到主线程,无需手动
Dispatchers.Main。
-
viewModelScope(ViewModel)viewModelScope.launch { … }- 与 ViewModel 生命周期绑定,ViewModel 清除时取消。
lifecycleScope vs. viewModelScope
| 特性 | lifecycleScope | viewModelScope |
|---|---|---|
| 所属组件 | ComponentActivity / Fragment | ViewModel |
| 绑定的生命周期 | 与 视图 生命周期绑定,onDestroy() 时取消 | 与 ViewModel 生命周期绑定,onCleared() 时取消 |
| 默认调度器 | Dispatchers.Main(主线程) | Dispatchers.Main.immediate(主线程、立即执行) |
| 典型用例 | 界面相关的协程:更新 UI、短时用户交互 | 与业务逻辑相关的协程:数据请求、缓存、长时计算 |
| 防止泄漏 | 页面销毁自动取消,避免在后台继续运行 | ViewModel 销毁自动取消,避免持久任务泄漏 |
六、 launch(并行) vs. withContext(串行)
-
launch { … }- 启动一个新的协程,不返回结果,只返回一个
Job; - 多次调用会并行执行,彼此之间不等待;
- 启动一个新的协程,不返回结果,只返回一个
-
withContext(dispatcher) { … }- 是一个挂起函数,会切换到指定调度器执行,并挂起当前协程直到代码块完成;
- 保证调用顺序——后续代码要等该块执行完再继续;
- 若
dispatcher与当前相同,则不会实际切线程。
示例:并行(非预期串行)
// 在 Main 调度器启动
CoroutineScope(Dispatchers.Main).launch {
// ① 这个 launch 启动一个子协程,但当前协程并不等它完成就结束
launch {
// 这里的“网络代码”会异步执行,其返回值不会被外层协程使用
val data = fetchNetworkData()
println("Inner data: $data")
}
// 外层协程到这里就已经结束,inner 仍在后台跑
}
示例:串行(正确顺序)
CoroutineScope(Dispatchers.Main).launch {
// 切到 IO 线程做网络请求,挂起当前协程直到完成
val data: String = withContext(Dispatchers.IO) {
fetchNetworkData()
}
// 切回 Default 线程做密集计算
val processed: String = withContext(Dispatchers.Default) {
processData(data)
}
// 回到 Main,安全更新 UI
println("Processed data: $processed")
}
七、线程切换规则
-
切换条件
- 只有在
withContext或其它挂起点指定了不同的CoroutineDispatcher时,才会发生真正的线程切换; - 相同调度器下的
withContext不触发额外切换。
- 只有在
-
IO vs. CPU 密集
- IO 密集(网络、磁盘、数据库)用
Dispatchers.IO; - CPU 密集(复杂计算、排序、加解密)用
Dispatchers.Default; - UI 更新、短任务用
Dispatchers.Main。
- IO 密集(网络、磁盘、数据库)用
-
避免卡顿
- 不要在
Main线程执行耗时逻辑,务必用withContext(Dispatchers.IO/Default)或在后台协程里执行。
- 不要在
-
Scope 选择
- 界面相关:用
lifecycleScope.launch { … },Activity/Fragment 销毁自动取消; - 业务/数据:用
viewModelScope.launch { … },ViewModel 清除自动取消; - 全局任务:慎用
GlobalScope,易造成泄漏。
- 界面相关:用
总结:
- 用
launch快速并行起协程;- 用
withContext串行挂起、切换线程并获取结果;- 选择合适的 Scope,结合调度器,才能写出既高效又可靠的协程代码。
挂起函数为什么不卡线程?
因为实际任务的执行是在另外的线程。并非当前的线程。切换的过程类似于回调的方式。
结构化并发
GCroot有哪些?对于这个问题,你只需要知道,一个对象在什么样的情况下不被回收,其实就能比较清楚了:
- 被Static引用的对象,static的生命周期是整个app的生命周期,因此他引用的对象都是不会被回收的
- 活跃的线程,就是runable或者running状态的线程。
- 来自JNI(Native)对象的引用,因为JNI已经脱离了Java的范畴,JVM无法判断这个对象是否可以被回收,因此统一认为他无法被回收。
一、GC Roots
在 JVM 中,对象只有在“不可达”时才会被回收。常见的 GC Roots 包括:
-
静态引用
- 被
static字段引用的对象,其生命周期与应用相同,JVM 不会回收。
- 被
-
活跃线程栈帧
- 处于 Runnable 或 Running 状态的线程的栈帧中引用的对象。
-
JNI(Native)引用
- 来自本地代码的引用不在 JVM 可控范围内,也被视为GC Root。
二、协程取消
// 启动一个协程并获得 Job
val job = lifecycleScope.launch {
launch { /* 子协程 A */ }
}
// 只取消 job 及其子协程
job.cancel()
// 取消整个 Scope(及其所有子协程)
// 一般不手动调用,因为 lifecycleScope 会在 onDestroy 自动取消
lifecycleScope.cancel()
job.cancel():取消该 Job 以及它启动的子协程。scope.cancel():取消所有由该 Scope 启动的协程。
三、协程线程切换
协程切换线程有两种方式:
-
launch启动 —— 并行执行
每次调用都会并行启动一个新的协程,互不等待。CoroutineScope(Dispatchers.Main).launch { // 第一个协程体 launch { // 这是第二个协程,与外层并行 val data = fetchNetworkData() println("Inner data: $data") } // 外层协程不会等待内层完成就结束 } -
withContext挂起 —— 串行执行
是挂起函数:切换到指定调度器执行、挂起当前协程,直到代码块完成,然后恢复。CoroutineScope(Dispatchers.Main).launch { // 切到 IO 线程,挂起当前协程直到完成 val data: String = withContext(Dispatchers.IO) { fetchNetworkData() } // 切到 Default 线程处理 val processed: String = withContext(Dispatchers.Default) { processData(data) } println("Processed data: $processed") }
注意:如果
withContext的调度器与当前相同,则 不会真正切线程。
四、结构化并发(Structured Concurrency)
Kotlin 协程通过父子关系和作用域,保证并发任务的可控、可取消、可组合。
4.1 launch vs. async
-
launch { … }- 启动一个不返回结果的协程,返回
Job,用于并行执行。
- 启动一个不返回结果的协程,返回
-
async { … }- 启动一个返回结果的协程,返回
Deferred<T>,通过await()获取结果。
- 启动一个返回结果的协程,返回
4.2. launch + async 示例
4.2.1 先 async 再 launch + coroutineScope
// 提前启动一个 Deferred,用于并行获取 contributors
val deferred = lifecycleScope.async {
gitHub.contributors("square", "retrofit")
}
lifecycleScope.launch {
// 添加这个代码的原因是为异常的结构化管理。
// 下边这三行中的 contributors1 与 contributors2 是并行的。不能添加lifecycleScope.,
// 否则会导致外层的lifecycleScope.无法管理内部的写成,因为加了lifecycleScope.就不是外部的子协程了。
coroutineScope {
// 这两行是并行执行的:一行直接调用,一行从 deferred 拿结果
val contributors1 = gitHub.contributors("square", "retrofit")
val contributors2 = deferred.await()
// 合并并展示
showContributors(contributors1 + contributors2)
}
}
-
要点
deferred和contributors1会并行发起网络请求。coroutineScope { … }确保内部子协程的异常能被统一捕获和处理。
4.2.2 推荐写法:在同一 coroutineScope 内启动两个 async
lifecycleScope.launch {
// 添加这个代码的原因是为异常的结构化管理。
// 下边这三行中的 deferred1 与 deferred2 是并行的。不能添加lifecycleScope.,
// 否则会导致外层的lifecycleScope.无法管理内部的写成,因为加了lifecycleScope.就不是外部的子协程了。
coroutineScope {
// 并行启动两个异步任务
val deferred1 = async { gitHub.contributors("square", "retrofit") }
val deferred2 = async { gitHub.contributors("square", "okhttp") }
// await() 会挂起直到各自完成,然后合并结果
showContributors(deferred1.await() + deferred2.await())
}
}
注意:
- 不要在内部再用
lifecycleScope.async,那样会脱离外层coroutineScope的管理,无法保证结构化并发。async { … }默认继承外层CoroutineScope。
4.2.3 等价的 CompletableFuture 写法(Java 风格)
private fun completableFutureStyleMerge() {
val future1 = gitHub.contributorsFuture("square", "retrofit")
val future2 = gitHub.contributorsFuture("square", "okhttp")
future1
.thenCombine(future2) { c1, c2 -> c1 + c2 }
.thenAccept { merged ->
handler.post { showContributors(merged) }
}
}
// Retrofit + CompletableFuture 的封装方法签名
fun contributorsFuture(
@Path("owner") owner: String,
@Path("repo") repo: String
): CompletableFuture<List<Contributor>>
4.3. 并行流程仅需顺序依赖(不需要结果)
lifecycleScope.launch {
// 两个初始化任务:一个 launch(无返回值),一个 async(可 await)
val initJob = launch { init() }
val initJob2 = async { init2() }
// 并行发起网络请求
val contributors1 = gitHub.contributors("square", "retrofit")
// 等待 initJob 完成(不取结果)
initJob.join()
// 等待 initJob2 完成,并取其结果(如果需要)
val init2Result = initJob2.await()
// 继续后续处理
processData(contributors1, init2Result)
}
-
区别
join():挂起当前协程直到目标Job完成,但不返回结果。await():挂起当前协程直到Deferred完成,并返回结果;若只是用来等待也可以,但必须用async启动。
五、join vs. await
-
Job.join()- 挂起当前协程直到目标协程完成,但不返回结果;
-
Deferred<T>.await()- 挂起当前协程直到目标协程完成,并返回结果。
示例:join 的执行流程
import kotlinx.coroutines.*
fun main() = runBlocking {
val job1 = launch {
delay(1_000L)
println("Job1 completed")
}
val job2 = launch {
println("Job2 waiting for Job1")
job1.join() // 等待 job1 完成
println("Job2 starts after Job1")
}
println("Main starts")
job2.join() // 等待 job2 完成
println("Main ends")
}
执行顺序:
runBlocking阻塞主线程,直到内部所有协程完成。job1延迟 1s 打印Job1 completed。job2先打印Job2 waiting for Job1,然后在job1.join()处挂起。job1完成后,job2恢复并打印Job2 starts after Job1。- 最后主协程在
job2.join()处挂起,待job2完成后打印Main ends。
在这个流程中:
job1是被等待的协程。job2调用了job1.join(),因此job2等待job1完成。- 主协程调用了
job2.join(),因此主协程等待job2完成。
调用join的协程会挂起自身的执行,直到目标协程完成。这是协程同步的一种方式,用于确保某些协程任务完成后再执行其他操作。其实也可以简单的理解为调用join的协程加入了另一个写成的执行流程。
协程与回调型API的协作:suspendCoroutine与suspendCancellableCoroutine
-
目的:将回调式(Callback)API 包装成挂起函数,便于在协程中串行调用并支持结构化并发。
-
区别:
suspendCoroutine不感知协程的取消;suspendCancellableCoroutine支持协程取消,并可在invokeOnCancellation中做清理。
suspendCoroutine 与 suspendCancellableCoroutine 是用来与线程世界做连接的,用他们就能把回调格式的api 转换成挂起函数。
1. 使用 suspendCoroutine 转换回调
// 将 Retrofit 的 Callback API 包装成挂起函数
suspend fun callbackToSuspend(): List<Contributor> =
suspendCoroutine { continuation -> // 指定类型 List<Contributor>
gitHub.contributorsCall("square", "retrofit")
.enqueue(object : Callback<List<Contributor>> {
override fun onResponse(
call: Call<List<Contributor>>,
response: Response<List<Contributor>>,
) {
// 结果的处理:恢复协程并返回数据
continuation.resume(response.body()!!)
}
override fun onFailure(call: Call<List<Contributor>>, t: Throwable) {
// 发生异常:恢复协程并抛出异常
continuation.resumeWithException(t)
}
})
// ⚠️ suspendCoroutine 不支持协程取消后的清理
}
调用示例
val job = lifecycleScope.launch {
try {
// 调用上面定义的挂起函数
val contributors = callbackToSuspend()
showContributors(contributors)
} catch (e: Exception) {
// 捕获并处理挂起过程中抛出的异常
infoTextView.text = e.message
}
}
2. 使用 suspendCancellableCoroutine 支持取消
suspend fun callbackToCancellableSuspend(): List<Contributor> =
suspendCancellableCoroutine { continuation ->
// 1) 发起 Retrofit 请求
val call = gitHub.contributorsCall("square", "retrofit")
call.enqueue(object : Callback<List<Contributor>> {
override fun onResponse(
call: Call<List<Contributor>>,
response: Response<List<Contributor>>,
) {
continuation.resume(response.body()!!)
}
override fun onFailure(call: Call<List<Contributor>>, t: Throwable) {
continuation.resumeWithException(t)
}
})
// 2) 注册协程取消后的收尾工作
continuation.invokeOnCancellation {
// 协程被取消时,取消底层网络请求
call.cancel()
}
}
调用示例
val job2 = lifecycleScope.launch {
try {
val contributors = callbackToCancellableSuspend()
showContributors(contributors)
} catch (e: Exception) {
infoTextView.text = e.message
}
}
-
suspendCancellableCoroutine:- 在挂起点内部,协程可以被取消;
invokeOnCancellation { … }中执行取消回调,实现对 “传统回调 API” 的收尾和清理。
协程内部调用传统式的回调函数
-
不要在协程内部再用
launch嵌套启动子协程来调用suspendCoroutine,否则外层的try…catch捕获不到suspendCoroutine中抛出的异常。 -
要在同一个协程体内直接调用
suspendCoroutine,才能让异常落到外层catch。 -
如果需要响应协程取消并做清理,则应使用
suspendCancellableCoroutine并在invokeOnCancellation中执行收尾工作。
协程内部调用传统式的回调函数:
1. 错误示例:嵌套 launch 无法捕获异常
lifecycleScope.launch {
try {
launch {
// ❌ 这里又用 launch,会启动一个新的子协程,
// 它的异常不会被外层的 try–catch 捕获
val contributors = suspendCoroutine<List<Contributor>> { cont ->
gitHub.contributorsCall("square", "retrofit")
.enqueue(object : Callback<List<Contributor>> {
override fun onResponse(
call: Call<List<Contributor>>,
response: Response<List<Contributor>>
) {
cont.resume(response.body()!!)
}
override fun onFailure(call: Call<List<Contributor>>, t: Throwable) {
cont.resumeWithException(t)
}
})
}
showContributors(contributors)
}
} catch (e: Exception) {
// 这里捕获不到上面 enqueue 回调里的异常
}
}
- 为什么捕获不到?
launch { … }启动子协程后立即返回,外层的try块已经结束,后续异常不会再被这个try捕获。
2. 正确示例:直接在协程体内使用 suspendCoroutine
lifecycleScope.launch {
try {
// ▶️ 直接把回调包装成挂起函数,异常会被下面的 catch 捕获
val contributors = suspendCoroutine<List<Contributor>> { cont ->
gitHub.contributorsCall("square", "retrofit")
.enqueue(object : Callback<List<Contributor>> {
override fun onResponse(
call: Call<List<Contributor>>,
response: Response<List<Contributor>>
) {
cont.resume(response.body()!!) // 恢复并返回数据
}
override fun onFailure(call: Call<List<Contributor>>, t: Throwable) {
cont.resumeWithException(t) // 抛出异常
}
})
}
showContributors(contributors)
} catch (e: Exception) {
// ✅ 能捕获 suspendCoroutine 内部抛出的异常
infoTextView.text = e.message
}
}
- 要点:在同一个协程体内调用
suspendCoroutine,外层try–catch才能拦截到回调中通过resumeWithException抛出的错误。
3. 响应取消并做清理:suspendCancellableCoroutine
suspend fun cancellableContributors(): List<Contributor> =
suspendCancellableCoroutine { cont ->
// 1) 发起网络请求
val call = gitHub.contributorsCall("square", "retrofit")
call.enqueue(object : Callback<List<Contributor>> {
override fun onResponse(
call: Call<List<Contributor>>,
response: Response<List<Contributor>>
) {
cont.resume(response.body()!!)
}
override fun onFailure(call: Call<List<Contributor>>, t: Throwable) {
cont.resumeWithException(t)
}
})
// 2) 注册协程取消时的收尾工作
cont.invokeOnCancellation {
// 协程被取消后,主动取消底层请求
call.cancel()
}
}
// 调用示例
val job2 = lifecycleScope.launch {
try {
val contributors = cancellableContributors()
showContributors(contributors)
} catch (e: Exception) {
infoTextView.text = e.message
}
}
- 为什么要用
suspendCancellableCoroutine?
它能让挂起的协程在 被取消 时执行invokeOnCancellation中的清理逻辑(如取消 RetrofitCall)。用suspendCoroutine则不会触发此回调。
// 协程内部调用传统式的回调函数
lifecycleScope.launch {
try { // 加入try catch 代码块是为了捕获suspendCoroutine内部的异常,但是这里我们亦可以不处理
launch { // 如果直接多launch进行try-catch,会无法捕获suspendCoroutine出现的异常,只能捕获启动协程的异常。
// 因为launch只是一个启动协程的过程,启动完成后,这个流程就结束了,意味着trycatch也结束了。
/*val contributors =
*//*下边这个代码可以抽出来,当做一个挂起函数来使用,然后上边的contributors就可以按照写成的串行方式传递下去了。*//*
suspendCoroutine<List<Contributor>>*//*指定类型,否则resume处会出现类型不匹配错误*//* {
gitHub.contributorsCall("square", "retrofit")
.enqueue(object : Callback<List<Contributor>> {
override fun onResponse(
call: Call<List<Contributor>>, response: Response<List<Contributor>>,
) {
it.resume(response.body()!!) // 结果的处理需要换成新的函数,这个是挂起函数的返回
// showContributors(response.body()!!)
}
override fun onFailure(call: Call<List<Contributor>>, t: Throwable) {
it.resumeWithException(t) //一旦发生异常,suspendCoroutine会立即结束。
}
})
}
showContributors(contributors)*/
}
val contributors =
/*下边这个代码可以抽出来,当做一个挂起函数来使用,然后上边的contributors就可以按照写成的串行方式传递下去了。*/
suspendCoroutine<List<Contributor>>/*指定类型,否则resume处会出现类型不匹配错误*/ {
gitHub.contributorsCall("square", "retrofit")
.enqueue(object : Callback<List<Contributor>> {
override fun onResponse(
call: Call<List<Contributor>>,
response: Response<List<Contributor>>,
) {
it.resume(response.body()!!) // 结果的处理需要换成新的函数,这个是挂起函数的返回
// showContributors(response.body()!!)
}
override fun onFailure(
call: Call<List<Contributor>>,
t: Throwable
) {
it.resumeWithException(t) //一旦发生异常,suspendCoroutine会立即结束。
}
})
}
showContributors(contributors)
} catch (e: Exception) { //捕获异常并进行处理。
}
}
注册取消之后的收尾工作:
val job2 = lifecycleScope.launch {
it.invokeOnCancellation {// 注册取消之后的收尾工作。
}
suspendCancellableCoroutine { // 在这个代码块里边执行的回调函数的协程(job2)可以被取消(但不会会终止这里边回调式api的执行,你需要再invokeOnCancellation内处理回调式api的后续工作)。使用suspendCoroutine时不行的。
}
}
一、回调式 API 转为可取消的挂起函数
- 如何将回调 API 变为可取消的挂起函数并做清理?
- 协程取消只是“状态标识”,只有挂起点才能识别并中断执行,阻塞调用则不会响应取消。
示例:
private suspend fun callbackToCancellableSuspend(): List<Contributor> =
suspendCancellableCoroutine { continuation ->
// 标记回调是否已处理过,防止重复清理
var callbackHandled = false
// 1) 注册协程取消时的收尾工作
continuation.invokeOnCancellation {
println("Coroutine cancelled")
// 只有回调尚未处理时,才执行清理
if (!callbackHandled) {
// 例如:取消 Retrofit 请求
gitHub.contributorsCall("square", "retrofit").cancel()
}
}
// 2) 发起回调式请求
gitHub.contributorsCall("square", "retrofit")
.enqueue(object : Callback<List<Contributor>> {
override fun onResponse(
call: Call<List<Contributor>>,
response: Response<List<Contributor>>
) {
// 协程仍然活跃时才恢复,并标记已处理
if (continuation.isActive) {
callbackHandled = true
continuation.resume(response.body()!!)
}
}
override fun onFailure(
call: Call<List<Contributor>>,
t: Throwable
) {
// 协程仍然活跃时才恢复,并标记已处理
if (continuation.isActive) {
callbackHandled = true
continuation.resumeWithException(t)
}
}
})
}
-
逻辑说明
- 先调用
invokeOnCancellation {…}注册取消回调; - 发起网络请求并在回调里调用
resume或resumeWithException; - 用
callbackHandled防止已响应的请求再次进入取消分支; - 若协程被外部
cancel(),且请求尚未回调完成,则在invokeOnCancellation中取消请求。
- 先调用
二、协程取消与阻塞调用对比
val job = lifecycleScope.launch {
println("Coroutine start")
delay(500) // 可被取消的挂起函数,会检测协程取消状态
Thread.sleep(500) // 阻塞调用,不会响应协程取消
println("Coroutine end")
}
lifecycleScope.launch {
delay(200)
job.cancel() // 仅设置取消状态,挂起函数(delay)会中断协程,Thread.sleep 不会
}
-
要点
- 挂起函数(如
delay、withContext、suspendCancellableCoroutine)会在恢复前检查协程的取消状态,一旦检测到取消则抛出CancellationException并终止后续逻辑。 - 阻塞方法(如
Thread.sleep)仅停住当前线程,不会抛出或响应协程的取消信号,协程仍会在阻塞结束后继续执行。
- 挂起函数(如
回到线程世界:runBlocking()
一、三种协程启动方式对比
| 启动器 | 返回类型 | 是否阻塞调用线程 | 典型用途 |
|---|---|---|---|
launch {…} | Job | 不阻塞 | 启动一个不返回结果的并发协程 |
async {…} | Deferred<T> | 不阻塞 | 启动一个可返回结果的并发协程 |
runBlocking {…} | 阻塞当前线程 | 阻塞 | 在普通函数或测试中从“挂起”桥接到阻塞 |
说明:“阻塞”指的是调用它的线程会被停住,直到内部所有协程体执行完毕才继续后续代码。
二、launch 创建新协程时的上下文继承
-
继承与复制
- 新协程基于父上下文复制大部分元素(调度器、名称等)。
-
Job层次- 每次
launch会创建一个新的Job,并作为父Job的子任务,从而在父取消时一并取消。
- 每次
-
调度器覆盖
- 如果显式传入如
Dispatchers.IO,新的上下文会用它覆盖父调度器;否则继承父调度器。
- 如果显式传入如
三、CoroutineScope 的作用
-
提供
CoroutineContext- 包含
ContinuationInterceptor(即Dispatcher),决定协程运行线程;
- 包含
-
管理生命周期
- 持有所有子协程的
Job,可一键取消整个 Scope 内的所有协程。
- 持有所有子协程的
四、runBlocking:从协程世界“回到”阻塞线程世界
kotlin
// 普通函数中启动协程并等待其完成
fun main() = runBlocking {
val data = withContext(Dispatchers.IO) {
// 调用阻塞式 API,但在 IO 线程执行
gitHub.contributors("square", "retrofit")
}
println("Received data: $data")
}
-
特点
- 无需显式
CoroutineScope:直接在阻塞函数中使用; - 阻塞调用线程:调用它的线程会被停住,直到所有挂起函数执行完。
- 桥接作用:把挂起函数包装为普通阻塞式调用,便于在不支持挂起的环境中使用。
- 无需显式
五、runBlocking 的典型场景
-
测试代码
- 单元测试框架通常是同步的,使用
runBlocking能顺序地执行挂起函数并等待结果。
- 单元测试框架通常是同步的,使用
-
阻塞式 API 包装
- 在普通(非挂起)函数中,临时桥接协程调用。
-
启动入口
- 在构建 DSL 或初始化逻辑中,需要在继续执行前等待协程任务完成。
六、与其他启动器的关系示例
lifecycleScope.launch(Dispatchers.Main.immediate) {
// ▶️ 会立即在主线程开始执行
showLoading()
}
println("这行不会被阻塞") // launch 不阻塞
runBlocking {
// ▶️ 这里会阻塞当前线程,直到挂起体完成
val contributors = gitHub.contributors("square", "retrofit")
showContributors(contributors)
}
// runBlocking 阻塞后,才会继续下面的代码
七、注意事项
- 不要在 UI 线程长时间使用
runBlocking,否则会导致界面卡顿。 - 生产环境优先用
launch/async+ 合适的Dispatcher,保持非阻塞。 - 测试或桥接场景下再考虑
runBlocking,以免误用造成性能问题。
学后测验
一、单项选择题(每题 2 分,共 6 题)
- 下列关于
Executor与CoroutineContext的描述,哪一项是正确的?
A. 两者都能携带CoroutineName
B.Executor只负责提交Runnable,不包含协程元素
C.CoroutineContext不能决定线程调度
D.Executor可以直接存放Job
【答案】B
【解析】Executor是 Java 线程池接口,只管把Runnable交给线程;CoroutineContext既能携带线程调度器(Dispatcher)又能保存Job、名称等。 Dispatchers.Default最适合执行哪类任务?
A. 大量数据库读写 B. 图片下载 C. CPU 密集型计算 D. 主线程 UI 更新
【答案】C
【解析】Default线程池大小≈CPU 核心数,专为计算密集场景设计。- 下面哪个 Scope 与 Activity/Fragment 的生命周期绑定?
A.GlobalScopeB.lifecycleScopeC.viewModelScopeD.CoroutineScope(Dispatchers.IO)
【答案】B
【解析】lifecycleScope在组件onDestroy()时自动cancel()。 withContext(Dispatchers.IO)相比launch(Dispatchers.IO)的主要区别是:
A. 前者并行,后者串行 B. 前者挂起等待完成 C. 后者会阻塞线程 D. 前者返回Job
【答案】B
【解析】withContext是挂起函数,会挂起直到代码块执行完;launch立即返回Job并并行执行。- 使用
suspendCoroutine包装回调 API 时,若协程被cancel(),默认行为是:
A. 自动抛出CancellationException并取消回调
B. 协程继续等待回调结果
C. 立即停止回调、不再返回
D. JVM 抛出InterruptedException
【答案】B
【解析】suspendCoroutine不感知取消;只有suspendCancellableCoroutine才能在取消时执行清理。 - 在
CoroutineContext合并 (+) 逻辑里,为什么要把ContinuationInterceptor放最外层?
A. 避免深拷贝 B. 方便最快速读取 Dispatcher C. 节省内存 D. 防止 GC
【答案】B
【解析】 协程恢复频繁读取 Dispatcher,放最外层可 O(1) 获取。
二、多项选择题(每题 3 分,共 6 题)
- 下列哪些属于常见 GC Roots?
A. 活跃线程栈帧 B. 被static引用的对象 C. JNI 引用 D. WeakReference
【答案】A B C
【解析】 WeakReference 对象本身可被回收,不是 GC Root。 GlobalScope.launch的特点有:
A. 协程 Job 的parent为null
B. 默认使用Dispatchers.Default
C. 会在 Activity 销毁时自动取消
D. 适合全局日志、心跳等任务
【答案】A B D
【解析】 与界面生命周期无关,需手动管理取消。- 下面哪些调用能确保外层
try-catch捕获回调中的异常?
A. 直接在协程体内调用suspendCoroutine
B. 在协程体内launch { suspendCoroutine{} }
C. 使用suspendCancellableCoroutine并在回调里resumeWithException
D. 在runBlocking里起线程执行回调
【答案】A C
【解析】 嵌套launch会把异常抛到子协程,外层try捕不到。 - 关于
supervisorScope,下列说法正确的有:
A. 内部子协程异常不会取消兄弟协程
B. 其根 Job 为SupervisorJob
C. 内部任一协程抛异常后supervisorScope不会向外抛异常
D. 适合并行任务需要失败隔离的场景
【答案】A B D
【解析】supervisorScope最终仍会把最后一个异常向上传递。 - 调用
withContext真的会发生线程切换的条件有:
A. 传入的 Dispatcher 与当前 Dispatcher 不同
B. 传入一个新的SupervisorJob
C. 当前协程已经被取消
D. 参数为Dispatchers.Unconfined
【答案】A D
【解析】 仅当调度器不同或使用Unconfined时会切换;换Job不影响线程。 suspendCancellableCoroutine提供的能力包括:
A. 能在协程取消时触发清理逻辑
B. 自动重试失败的回调请求
C. 允许继续使用resume/resumeWithException
D. 内置超时机制
【答案】A C
【解析】 重试与超时需开发者自行实现。
三、判断题(每题 1 分,共 4 题)
Dispatchers.IO线程池大小固定为 64。
【答案】×
【解析】 默认上限≈64,可按阻塞情况动态伸缩。runBlocking会阻塞调用它的线程,因此不应该在 Android 主线程长时间使用。
【答案】√
【解析】 主线程被阻塞会导致 ANR。- 在
CoroutineContext中,同一种 Key 的元素只能存在一份,后加的会替换先加的。
【答案】√ withContext(EmptyCoroutineContext)与coroutineScope {}的运行语义完全等价。
【答案】√
【解析】 都不会切换调度器,只是挂起直至代码块结束。
四、简答题(每题 5 分,共 4 题)
-
解释
launch与withContext在“并行 / 串行”方面的根本差异,并各给一个典型使用场景。
【答案】launch启动新协程立即返回Job,外层继续执行 ⇒ 并行;适合并发发送多条日志、无返回值的后台任务。withContext会挂起当前协程直至代码块完成 ⇒ 串行;适合“切线程 + 等结果”场景,如先 IO 下载再回主线程更新 UI。
-
为何推荐在 Activity/Fragment 中使用
lifecycleScope而不是手写GlobalScope?
【答案】lifecycleScope内建根Job与组件生命周期绑定,onDestroy自动取消子协程,避免内存泄漏;GlobalScope协程无父 Job,除非手动取消,否则页面销毁后仍会在后台运行。 -
简述将回调式网络请求封装成可取消挂起函数的关键步骤。
【答案】- 使用
suspendCancellableCoroutine创建挂起点; - 在回调成功时
resume(value),失败时resumeWithException(e); - 调用
invokeOnCancellation注册协程取消后的清理逻辑(如call.cancel()); - 可用布尔标记防止回调与取消重复执行。
- 使用
-
ContinuationInterceptor总被放在CoroutineContext最外层有什么性能意义?
【答案】 取调度器是协程恢复的高频操作;置顶后查询只需一次哈希 / 一层解包即可 O(1) 获取,避免在深度嵌套的CombinedContext中线性查找。
五、编程题(共 2 题,每题 8 分)
-
封装回调 API
将下列 Retrofit 回调接口封装为 可取消 的挂起函数suspend fun fetchContributors(owner:String,repo:String):List<Contributor>。要求:- 取消协程时能同时取消 Retrofit
Call; - 网络失败时把异常抛给调用者。
interface GitHub { @GET("/repos/{owner}/{repo}/contributors") fun contributorsCall( @Path("owner") owner: String, @Path("repo") repo : String ): Call<List<Contributor>> }【答案代码(核心片段)】
suspend fun GitHub.fetchContributors( owner: String, repo : String ): List<Contributor> = suspendCancellableCoroutine { cont -> val call = contributorsCall(owner, repo) call.enqueue(object : Callback<List<Contributor>> { override fun onResponse( call: Call<List<Contributor>>, response: Response<List<Contributor>> ) { if (cont.isActive) cont.resume(response.body()!!) } override fun onFailure(call: Call<List<Contributor>>, t: Throwable) { if (cont.isActive) cont.resumeWithException(t) } }) cont.invokeOnCancellation { call.cancel() } }【解析】
suspendCancellableCoroutine使协程取消时触发invokeOnCancellation,从而安全取消底层网络请求。 - 取消协程时能同时取消 Retrofit
-
结构化并发处理
在viewModelScope内并行请求 “retrofit” 与 “okhttp” 的 contributors,然后排序后更新 UI。要求:- 使用结构化并发;
- 能捕获任一请求失败并在同一处处理;
- 不得在内部写
viewModelScope.async(要保持同一作用域)。
【答案代码(核心片段)】
viewModelScope.launch { try { coroutineScope { val d1 = async { api.fetchContributors("square", "retrofit") } val d2 = async { api.fetchContributors("square", "okhttp") } val merged = (d1.await() + d2.await()) .sortedByDescending { it.contributions } _uiState.value = merged // LiveData / StateFlow 更新 } } catch (e: Exception) { _uiState.value = emptyList() // 统一错误处理 } }【解析】
coroutineScope {}保证两个async同属一棵协程树,异常可一次catch;- 若内部改用
viewModelScope.async将脱离当前coroutineScope,无法集中管理异常。