CoroutineContext
一、CoroutineContext(协程上下文)
-
概念:承载协程运行所需的所有元信息,底层是一个键值对集合(
Element)。 -
常见元素:
Job:管理协程的生命周期与父子关系;ContinuationInterceptor(通常是各种Dispatcher):决定协程恢复在哪个线程或线程池。
-
作用:在协程挂起/恢复时,携带和查找执行所需的信息。
二、CoroutineScope
1. 存储并提供上下文
-
每个
CoroutineScope都持有一个coroutineContext: CoroutineContext,并将其用于新协程的启动。 -
coroutineContext中通常包含:- 一个
Job(或SupervisorJob),用以组织该 Scope 下所有协程的父子关系; - 至少一个
ContinuationInterceptor,决定默认的调度器; - 其他可选元素,如
CoroutineName、CoroutineExceptionHandler等。
- 一个
val scope = CoroutineScope(Dispatchers.IO)
// 等同于:
// 如果传入的 context 中没有 Job,就自动加一个新的 Job
// public fun CoroutineScope(context: CoroutineContext) =
// ContextScope(if (context[Job] != null) context else context + Job())
2. 启动协程
launch { … }、async { … }、actor { … }等都是CoroutineScope的扩展函数。- 启动新协程时,这些函数会“取用”当前 Scope 的
coroutineContext,并在其基础上创建子Job与(可选)调度器(取决于是否自己写了新的不同的调度器)覆盖。
scope.launch {
// this.coroutineContext[Job] → 子 Job
// coroutineContext[ContinuationInterceptor] → Dispatcher
}
三、两者的关系与职责
| 方面 | CoroutineContext | CoroutineScope |
|---|---|---|
| 核心含义 | 一组协程运行时元素(Job、Dispatcher…) | 持有并管理一个 CoroutineContext |
| 主要职责 | 保存协程执行所需的信息 | 提供上下文;启动协程;取消所属协程群 |
| 生命周期管理 | 由 Job 元素负责 | 通过内部的根 Job 级联管理所有子协程 |
| 启动协程时的作用 | 决定父子关系与线程分发 | 为 launch/async 提供上下文来源 |
四、总结
CoroutineContext:是协程运行时的“配置”,包含了线程调度、异常处理、父子关系等元素。CoroutineScope:是“持有”这份配置的容器,并以此来启动、管理和取消一组协程。
这种分离设计,使得协程能够在保持层次结构和生命周期可控的同时,灵活指定执行线程与异常策略。
GlobalScope
一、什么是 GlobalScope
-
GlobalScope是一个 单例对象,它本身实现了CoroutineScope:public object GlobalScope : CoroutineScope { override val coroutineContext: CoroutineContext get() = EmptyCoroutineContext } -
与普通
CoroutineScope(如CoroutineScope(Dispatchers.IO))不同,它的coroutineContext为空,即没有内置的Job。
二、GlobalScope.launch/async 的上下文构建
-
调用
GlobalScope.launch {…}时,实际用到的上下文是:// 伪代码,简化自源码 fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, block: suspend ()->Unit ): Job { // 合并三个部分:Scope 的 context + 默认 Dispatcher + 新 Job val newContext = this.coroutineContext + Dispatchers.Default + Job() // ……启动协程…… } -
因为
GlobalScope.coroutineContext是EmptyCoroutineContext,所以合并后:- 没有父 Job,新协程拿到的
Job()就是“根” - 使用了默认的调度器(
Dispatchers.Default)
- 没有父 Job,新协程拿到的
-
结果:
val job = GlobalScope.launch { /*…*/ } println(job.parent) // null println(job is Job) // true println(GlobalScope.coroutineContext[Job]) // null启动的协程既没有父协程,也不会被
GlobalScope的“Job”所管理。
三、与普通 CoroutineScope 的区别
| 特性 | GlobalScope | CoroutineScope(…) |
|---|---|---|
coroutineContext | EmptyCoroutineContext | 传入的 CoroutineContext(含 Job) |
| 启动协程时的父 Job | 无(新协程独立) | 会创建并附加到该 Scope 的根 Job |
| Scope 取消时对协程的影响 | 无,从不取消已启动的协程 | 取消 Scope 时会级联取消所有子协程 |
| 典型用途 | “真正全局”、与任何生命周期无关的顶层任务 |
为什么要这样设计?
- 解耦父子关系:
GlobalScope启动的协程独立存在,不会因任何上层作用域取消而被取消。 - 适合“常驻”任务:例如后台日志、全局单例服务,或者应用级别的定时器,不应被局部生命周期所影响。
// 1. GlobalScope 本身没有 Job
println(GlobalScope.coroutineContext[Job]) // null
// 2. 但 launch/async 会给协程创建一个独立的 Job
val job = GlobalScope.launch {
// 这里的 coroutineContext[Job] 是调用 launch 时新创建的 Job
println(coroutineContext[Job] != null) // true
}
println(job.parent) // null
// 3. 普通 Scope 会自动添加根 Job
val scope = CoroutineScope(Dispatchers.IO)
val job2 = scope.launch {
println(coroutineContext[Job] != null) // true
}
println(job2.parent) // 不为 null,属于 scope 的根 Job
val job0 = GlobalScope.launch {
coroutineContext[Job] // 该值不为空,因为用到的是CoroutineScope,而不是GlobalScope
}
val job = GlobalScope.async {
delay(1000)
coroutineContext[Job] // 该值不为空,因为用到的是CoroutineScope,而不是GlobalScope
}
GlobalScope.coroutineContext.job // 该值为空
GlobalScope.coroutineContext[Job] // 该值为空
// 自己创建的CoroutineScope也会有Job对象,源码自动添加的。
CoroutineScope(EmptyCoroutineContext).launch {
coroutineContext[Job]
}
// 通过GlobalScope的launch函数启动的协程的CoroutineScope是没有Job的,源码里边直接返回了空的context.
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
/**
* An empty coroutine context.
*/
@SinceKotlin("1.3")
public object EmptyCoroutineContext : CoroutineContext, Serializable {
private const val serialVersionUID: Long = 0
private fun readResolve(): Any = EmptyCoroutineContext
public override fun <E : Element> get(key: Key<E>): E? = null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R = initial
public override fun plus(context: CoroutineContext): CoroutineContext = context
public override fun minusKey(key: Key<*>): CoroutineContext = this
public override fun hashCode(): Int = 0
public override fun toString(): String = "EmptyCoroutineContext"
}
这样会导致 GlobalScope 有什么特点呢?一个CoroutineScope没有Job,那么它创建的协程就没有父协程了。它创建的协程的Job就没有父Job了。例如下边这个Job的parent就是空的:
val job = GlobalScope.async {
delay(1000)
}
GlobalScope 的这个特点有什么用处呢?他需要这个效果才能正常工作。如果我们需要某个协程一直存在,可以用下边这种方式:
CoroutineScope(EmptyCoroutineContext).launch {
}
使用GlobalScope需要注意
- 避免滥用:
GlobalScope启动的协程不会自动取消,容易造成内存泄漏或过期任务未停止。 - 推荐做法:绝大多数情况下应使用与生命周期或业务逻辑绑定的
CoroutineScope(如viewModelScope、lifecycleScope、手动创建并持有的CoroutineScope)。 - 仅在真正需要“脱离任何生命周期”的全局任务时,再考虑使用
GlobalScope。
从挂起函数里获取CoroutineContext
直接引入以下包,就可以在挂起函数中访问 coroutineContext:
import kotlin.coroutines.coroutineContext
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch(Dispatchers.Main) {
showDispatcher()
}
delay(10000)
}
private suspend fun showDispatcher() {
delay(1000)
println("Dispatcher: ${coroutineContext[ContinuationInterceptor]}")
}
/**
* 返回当前协程的上下文。
*/
@SinceKotlin("1.3")
@Suppress("WRONG_MODIFIER_TARGET")
@InlineOnly
public suspend inline val coroutineContext: CoroutineContext
get() {
throw NotImplementedError("Implemented as intrinsic")
}
为了在挂起函数里拿到当前协程作用域(CoroutineScope)所提供的上下文,Kotlin 还提供了一个函数 currentCoroutineContext()。
在某些场景下,直接使用顶层属性 coroutineContext 会与作用域里的同名属性冲突。例如:
GlobalScope.launch {
flow<String> {
// 这里的 coroutineContext,会解析为 launch 提供的上下文
coroutineContext
// 如果想取 flow 本身的大括号里挂起函数的上下文,就要用:
currentCoroutineContext()
}
}
——原因在于,成员属性的解析优先级高于顶层属性,所以在双重环境(launch 作用域 + flow 挂起函数)中,coroutineContext 总是指向 launch 的上下文。
下面用一个完整示例演示在 flow { … } 中,coroutineContext 与 currentCoroutineContext() 在不同调度器下的差异:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
GlobalScope.launch {
flow<String> {
println("Before flowOn:")
println(" coroutineContext = $coroutineContext")
println(" currentCoroutineContext() = ${currentCoroutineContext()}")
emit("Hello")
}
.flowOn(Dispatchers.IO) // 将发射数据的上下文切换到 IO 调度器
.collect { value ->
println("In collect:")
println(" value = $value")
println(" coroutineContext = $coroutineContext")
println(" currentCoroutineContext() = ${currentCoroutineContext()}")
}
}
// 等待足够时间让示例跑完
delay(3000)
}
- 在
flowOn(Dispatchers.IO)之前,coroutineContext和currentCoroutineContext()都是运行在runBlocking的上下文(默认是主线程)。 flowOn(…)会把上游的发射操作移动到指定调度器(这里是IO),此时在flow { … }里打印的两者仍然一致(都等于 IO 调度器)。- 在
collect { … }阶段,两者又都回到原始的launch或runBlocking上下文(例如主线程)。
因此,简单场景下两者行为相同;但当你需要区分挂起函数内部与外部作用域的上下文时,就应使用 currentCoroutineContext()。
coroutineScope()与SupervisorScope()
coroutineScope vs. launch 的区别
coroutineScope() 与 launch 在表面上都能“启动”子协程,但它们有两点本质区别:
-
上下文可定制性
launch可以接收一个CoroutineContext(比如不同的调度器或自定义的Job);coroutineScope()无任何参数,始终沿用外部上下文,相当于无参的launch。
-
挂起 vs. 立刻返回
launch和async启动协程后会立即返回一个Job或Deferred,并不会等待内部逻辑执行完毕;coroutineScope()是一个 挂起函数,它会挂起调用它的协程,直到其代码块中所有操作(包括任何通过launch或async启动的子协程)都执行完毕后才返回。
可将它理解为:
// 等同于:launch { … }.join()
coroutineScope {
launch { /* … */ }
// 只有当上面这行 launch 完全执行结束后,coroutineScope 才会返回
}
正是因为这个“挂起等待”的特性,coroutineScope() 常被用来在挂起函数内部:
- 启用子协程 —— 给挂起函数一个
CoroutineScope,才能启动launch、async等; - 封装并发逻辑并集中异常处理 —— 内部子协程失败会向外抛出,外层可以一处
try–catch,防止整个协程树无差别崩溃。
scope.launch {
// 沿用外部的coroutineContext,但不用外部的Job,而是创建一个新的Job来用
coroutineScope {
launch{ // coroutineScope还会等着它执行结束
}
}
launch {
}
}
那么什么情况下我们会想到使用coroutineScope呢?
使用场景一:在挂起函数内部启动子协程
-
问题:普通的
suspend fun并没有显式的CoroutineScope,无法直接调用launch、async等协程构建器。 -
传统做法:将挂起函数声明为
suspend fun CoroutineScope.someFun(),这样就能拿到this作用域:private suspend fun CoroutineScope.someFun() { println("scope(1) 的 context = $coroutineContext") // (1) launch(Dispatchers.IO) { println("launch(2) 的 context = $coroutineContext") // (2) } }此时
(1)是外层作用域的上下文,(2)则是IO调度器。 -
更优方案:直接在普通挂起函数中调用
coroutineScope { … },让 Kotlin 知道“这是一个新作用域”,即可在其中启动子协程:private suspend fun someFun() = coroutineScope { // 此处就拥有一个 CoroutineScope,可以直接 launch/async launch(Dispatchers.IO) { // ... } }
使用场景二:封装并发逻辑并统一返回值与异常
-
特点:
coroutineScope { … }会返回其代码块最后一行的结果,而launch本身不返回值。 -
示例:并行启动两个
async,组合它们的返回值:val combined: String = coroutineScope { val d1 = async { "rengwuxian" } val d2 = async { "AAAAAAAAA" } // 代码块的最后一行即为 coroutineScope 的返回值: "${d1.await()} ${d2.await()}" } println(combined) // 输出 "rengwuxian AAAAAAAAA" -
异常隔离:如果任一
async抛出非CancellationException,整个coroutineScope会向外抛异常,外层可以通过try–catch统一处理,避免各并发分支互相影响,也避免整个协程树无差别崩溃。
val result = try {
coroutineScope {
val d1 = async { fetchA() }
val d2 = async { fetchB() }
"${d1.await()} + ${d2.await()}"
}
} catch (e: Exception) {
"Error: ${e.message}"
}
// 即便内部某个请求失败,外层依然可以继续执行
println(result)
通过这两种典型用法,coroutineScope() 在挂起函数内部既能“造”一个作用域,又能将并发结果和异常收敛到同一个挂起点,极大地方便了结构化并发和异常管理。
为什么 coroutineScope 能隔离异常
- 串行挂起
coroutineScope是一个挂起函数,调用它的外层协程会被挂起,直到内部所有代码(包括通过launch/async启动的子协程)执行完毕后才恢复。这就像把并发逻辑“包裹”在一个串行块里,外层可以在它上面统一try–catch。 - 父子/兄弟协程并行时无法互相捕获
普通的launch(或默认的父子关系)和async并行运行时,相互之间没有 try/catch 绑定。一旦某个分支抛出非CancellationException,就会按“全面崩溃”规则取消整个协程树,外层也无法局部捕获。
示例一:在 coroutineScope 中捕获并处理异常
scope.launch {
// 一个并行的子协程,仅用于测时
val startTime = System.currentTimeMillis()
launch {
delay(2000)
println("Duration of coroutineScope: ${System.currentTimeMillis() - startTime} ms")
}
// 下面用 coroutineScope 包裹并发逻辑,外层能捕获内部异常
val name = try {
coroutineScope {
val d1 = async { "rengwuxian" }
val d2 = async<String> { throw RuntimeException("Error!") }
// 仅当两者都成功时才返回拼接结果
"${d1.await()} ${d2.await()}"
}
} catch (e: Exception) {
// 捕获到 "Error!",而不会导致 scope.launch 崩溃
e.message
}
println("Result name = $name")
}
- 这里即使
d2抛异常,也只是让coroutineScope向外抛,并被外层的try–catch捕获,scope.launch可以继续执行后续逻辑。
示例二:在 Android lifecycleScope 中结构化并发
lifecycleScope.launch {
try {
// 为两次网络请求提供一个独立挂起块,方便统一异常处理
coroutineScope {// 添加这个代码的原因是为异常的结构化管理。
val c1 = async { gitHub.contributors("square", "retrofit") }
val c2 = async { gitHub.contributors("square", "okhttp") }
// 并行请求完毕后合并结果并展示
showContributors(c1.await() + c2.await())
}
} catch (e: Exception) {
// 请求任一失败,就在这里集中处理
showError(e)
}
// launch 仍可继续执行其他逻辑
}
不要在这两个
async前加lifecycleScope.,否则它们就不再是当前coroutineScope { … }挂起块内部的子协程,coroutineScope无法管理它们。
加上lifecycleScope前缀的async会脱离这层局部作用域,变成外层lifecycleScope的子协程,从而失去并发封装和异常隔离的好处。
当我们在 coroutineScope { … } 里这样写:
coroutineScope {
val c1 = async { /*…*/ }
val c2 = async { /*…*/ }
// …
}
其实是这样执行的:
-
coroutineScope { … }会创建一个 新的CoroutineScope,其coroutineContext中包含一个 专属的Job(记作jobInner)。 -
在这个作用域里调用 扩展函数
async { … },它等同于:this.async { … } // this 就是 coroutineScope 提供的那个 CoroutineScope所以生成的两个
Deferred,它们的父 Job 都是jobInner,属于同一个作用域,coroutineScope能够:- 等待它们全部完成(因为它会挂起到所有子协程结束),
- 在任一失败时取消它们并把异常抛到外层。
但是如果我们写成:
coroutineScope {
val c1 = lifecycleScope.async { /*…*/ }
val c2 = lifecycleScope.async { /*…*/ }
// …
}
那 async 的 调用者 就变成了 lifecycleScope:
lifecycleScope自己持有一棵独立的协程树,其根 Job 我们记作jobLife。lifecycleScope.async { … }创建出来的Deferred都归到jobLife下——而不是jobInner。- 因此,
coroutineScope的那个jobInner并 不管理c1、c2,它也不会等待它们完成,更不会在你捕获到异常时自动取消它们。
简而言之:
- 不加前缀:
async { … }⇒ 生成的协程属于当前的coroutineScope,受它管理。 - 加上
lifecycleScope.:async { … }⇒ 生成的协程属于外层的lifecycleScope,脱离了内层coroutineScope的控制。
这就是为什么一旦用 lifecycleScope.async,就失去了“在同一挂起块内并行启动 + 挂起等待 + 统一异常处理”的结构化并发能力。
supervisorScope vs. coroutineScope
| 特性 | coroutineScope (普通) | supervisorScope |
|---|---|---|
| Job 语义 | 普通 Job | SupervisorJob 机制 |
| 子协程失败行为 | 任一失败 → 取消所有同级及子协程 → 向上抛异常 | 子协程失败 → 仅取消自身;兄弟协程继续执行;最后向上抛异常 |
| 适用场景 | 希望一处捕获内部所有并发异常 | 希望兄弟之间互不影响,单个失败不影响其他并发任务 |
scope.launch {
supervisorScope {
launch { /* A */ }
launch { throw RuntimeException("B failed") }
launch { /* C */ } // 即使 B 异常,C 仍会执行
}
// supervisorScope 本身会在内部所有子协程结束后再返回,并向外抛最后的异常
}
核心思路是:
coroutineScope:串行挂起 + 并发包裹 + 集中异常;- 普通父子/兄弟协程:并行运行,异常会级联导致全面崩溃;
supervisorScope:为并行任务提供“兄弟互不影响”的失败隔离。
再谈 withContext
1. 基本定义对比
suspend fun <R> coroutineScope(
block: suspend CoroutineScope.() -> R
): R { … }
- 无参数,沿用当前
coroutineContext。 - 挂起函数,会创建一个新的
ScopeCoroutine,并在其内部执行block,直到所有子协程完成才返回。
suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T { … }
-
带参数,可以传入新的
CoroutineContext(如不同的调度器或 Job)。 -
同样是挂起函数,会根据新旧上下文是否相同选择不同的执行路径:
- 完全相同:直接在当前线程、当前上下文中“无调度”执行(
ScopeCoroutine)。 - 同一 Dispatcher、仅其他属性不同:切换上下文元素但不切换线程(
UndispatchedCoroutine)。 - Dispatcher 也变化:切换到新调度器/线程(
DispatchedCoroutine)。
- 完全相同:直接在当前线程、当前上下文中“无调度”执行(
因此,综上,我们可以得到以下的结论:
-
withContext确实是挂起函数,跟coroutineScope一样,会等它内部的block完全执行完才返回; -
它必须带一个
CoroutineContext参数——这是它和coroutineScope最大的区别。 -
如果你传入的上下文跟当前协程上下文完全“对等”——
- 传
EmptyCoroutineContext:newContext = oldContext.newCoroutineContext(Empty)会得到原来的oldContext; - 传
coroutineContext:显式传入当前上下文,本质上也没变;
在这两种情况下,withContext会走“快速路径”,在同一线程、同一Job下执行,行为就跟coroutineScope { … }一模一样。
- 传
小结:
- 想切换调度器或引入新
Job就用withContext(Dispatchers.IO)、withContext(SupervisorJob())等;- 只是想在挂起函数中开启一个子作用域但不换上下文,可写成
withContext(EmptyCoroutineContext)或withContext(coroutineContext),这时它和coroutineScope { … }没任何差别。
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
// Contract: block is called exactly once
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
// Suspend without interception and return result via continuation uCont
return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
// 1. 原协程上下文
val oldContext = uCont.context
// 2. 基于传入的 context,创建一个新的上下文(可能包含原有元素 + 新元素)
val newContext = oldContext.newCoroutineContext(context)
// 3. 检查新上下文的取消状态
newContext.ensureActive()
// —— 快速路径 #1 ——
// 如果 newContext 与 oldContext 引用相同,说明没有任何上下文切换
if (newContext === oldContext) {
// 直接用 ScopeCoroutine,在同一线程、同一 Dispatcher 下执行
val coroutine = ScopeCoroutine(newContext, uCont)
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
// —— 快速路径 #2 ——
// 如果 Dispatcher 相同,但其他上下文元素(如 Job)变化
if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
// 用 UndispatchedCoroutine:切换 context,但不换线程
val coroutine = UndispatchedCoroutine(newContext, uCont)
// 临时安装新的上下文元素到当前线程,然后执行 block
withCoroutineContext(coroutine.context, null) {
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
// —— 慢速路径 ——
// Dispatcher 也变化,需要切换到 newContext 对应的线程/线程池
val coroutine = DispatchedCoroutine(newContext, uCont)
// 启动可取消的协程,开始执行 block
block.startCoroutineCancellable(coroutine, coroutine)
// 阻塞等待结果(挂起返回)
coroutine.getResult()
}
}
public suspend fun <R> coroutineScope(
block: suspend CoroutineScope.() -> R
): R {
// Contract: block is called exactly once
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
// Suspend without interception and return result via continuation uCont
return suspendCoroutineUninterceptedOrReturn { uCont ->
// 1. 创建一个 ScopeCoroutine,继承原有 uCont.context
val coroutine = ScopeCoroutine(uCont.context, uCont)
// 2. 立即启动并“无调度”(undispatched),执行 block
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
如果你想在不启动额外并行子协程的情况下切换上下文,就不要用
launch+async,而用withContext。
withContext是一个挂起函数,支持传入新的CoroutineContext(如不同的调度器、不同的Job等),内部会“串行”地执行你的逻辑,不会创建与外层并行的子协程。- 从使用体验看,它就像一个支持参数化上下文的
coroutineScope。- 也可以把它理解为一个“可定制上下文的串行化
launch+join”。
// 切换到 IO 调度器,并在该上下文中串行执行 block
withContext(Dispatchers.IO) {
// 这里的逻辑跑在 IO 线程池,执行完成后才回到外层上下文
doBlockingIOWork()
}
简而言之:
withContext= “可定制coroutineContext的coroutineScope挂起函数”,
从效果上相当于 “串行版的launch+async(或launch+join)”。
CoroutineName
主要是用于调试和测试的,也是一个coroutineContext,如下边的代码,所有通过scope启动的协程都有这个name。
val scope = CoroutineScope(Dispatchers.IO + name)
scope.launch {
}
CoroutineContext 的加减和 get()
一、CoroutineContext 的合并(+)与剔除(-)
1.1 + 的基本行为
val scope = CoroutineScope(
Dispatchers.IO
+ Job()
+ CoroutineName("MyCoroutine")
+ job2
)
-
每次用
+都会将左侧已有的CoroutineContext与右侧元素合并,返回一个新的CombinedContext。 -
合并是“层层套上去”的结构,内部可能表现为:
CombinedContext(CombinedContext(Dispatchers.IO, Job()), CoroutineName("MyCoroutine"))- 或者等价的另一种嵌套顺序,但对外效果一致。
1.2 同类型元素的替换
-
如果右侧元素与左侧上下文中已有同类型(同 key)的元素,就 替换 左侧旧的:
CoroutineScope(Dispatchers.IO + Job() + Job()) // 结果中只会保留第二个 Job,第一个被剔除 -
这是因为
Job自身定义了:@Deprecated(..., level = DeprecationLevel.ERROR) public operator fun plus(other: Job): Job = other即对两个
Job做 “+” 时,直接取右边那个。
另外还有一些细节需要说明下:
/**
* Returns a context containing elements from this context and elements from other [context].
* The elements from this context with the same key as in the other one are dropped.
*/
public operator fun plus(context: CoroutineContext): CoroutineContext =
// 空的EmptyCoroutineContext不加入到ConbineContext里边
if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation,
context.fold(this) { acc, element ->
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
// make sure interceptor is always last in the context (and thus is fast to get when present)
// 让ContinuationInterceptor总是排在最外层,这样查找的时候就可以省时间了。因为只需要拨开一层conbinedContext就行了
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
-
跳过空上下文:
EmptyCoroutineContext + X会直接返回X。 -
按元素折叠:逐个把
context里的元素 fold 进累计器acc。 -
剔除同 Key:合并时遇到同 Key 的元素,旧的被剔除,只保留新的。
-
优化拦截器位置:
ContinuationInterceptor(即CoroutineDispatcher)总是放在最外层,查询时只需“拨开”一层。
总而言之
-
+操作做了两件事- 合并:将左侧
CoroutineContext和右侧元素/上下文折叠成一个新的CombinedContext,相当于把它们“套”在一起。 - 替换同类型元素:合并时,如果右侧有跟左侧相同 Key(类型)的元素,旧的(左侧的)会被剔除,只保留新的(右侧的)。
- 合并:将左侧
-
为什么可以写
coroutineContext[Job]而不是Job.Key?
在 Kotlin 的协程 API 里,Job接口声明了一个companion object Key,它本身就是CoroutineContext.Key<Job>:public interface Job : CoroutineContext.Element { companion object Key : CoroutineContext.Key<Job> }因此,写作
coroutineContext[Job],Kotlin 就会把Job解析为它的伴生对象Key,这是语言层面的简写支持。等价的完整写法是:
// 简写 val job1: Job? = coroutineContext[Job] // 全写 val job2: Job? = coroutineContext[Job.Key]
二、从 CoroutineContext 中获取元素
scope.launch {
// 1. 通过简写的 Key(Job.Companion)获取当前 Job
val job1: Job? = coroutineContext[Job]
// 2. 等价的全写形式
val job2: Job? = coroutineContext[Job.Key]
// 以下方式均不可用(它们不是 CoroutineContext.Key<T>)
// val job3: Job? = coroutineContext[Job::class]
// val job4: Job? = coroutineContext[Job::class.java]
}
-
Job接口中定义了它自己的伴生对象Key:public interface Job : CoroutineContext.Element { companion object Key : CoroutineContext.Key<Job> } -
Kotlin 支持简写,因此
coroutineContext[Job]会被编译器视作coroutineContext[Job.Key]。
三、调用 CoroutineDispatcher 的特有 API
// 1. 以 ContinuationInterceptor(CoroutineDispatcher 的别名)获取调度器
val dispatcher1: CoroutineDispatcher? =
coroutineContext[ContinuationInterceptor] as? CoroutineDispatcher
// 2. 也可以直接用接口类型的 Key
val dispatcher2: CoroutineDispatcher? =
coroutineContext[CoroutineDispatcher]
// 3. 或者更显式地调用 get()
val dispatcher3: CoroutineDispatcher? =
coroutineContext.get(CoroutineDispatcher)
扩展:生成一个限并行度(parallelism)更小的 Dispatcher
// 假设 dispatcher 是上面任一方式获取到的 CoroutineDispatcher
val limited: CoroutineDispatcher? = dispatcher?.limitedParallelism(3, name = "limited-3")
-
limitedParallelism(parallelism: Int, name: String? = null)会基于原有 Dispatcher 创建一个新的LimitedDispatcher,- parallelism:允许同时并发的最大协程数;
- name:可选的线程名后缀,便于调试。
public open fun CoroutineDispatcher.limitedParallelism(
parallelism: Int,
name: String? = null
): CoroutineDispatcher {
parallelism.checkParallelism() // 校验参数合法
return LimitedDispatcher(this, parallelism, name)
}
这样,你可以:
- 简洁地 用
coroutineContext[Job]或coroutineContext[CoroutineDispatcher]拿到当前协程的Job和调度器; - 灵活地 用
limitedParallelism(...)派生出一个并发度受限的新调度器。
四、从 CoroutineContext 中获取元素(get())
可以通过元素的 Key 对象 来检索:
// 获取当前 Job
val job: Job? = coroutineContext[Job]
// 等价于
val job2: Job? = coroutineContext[Job.Key]
// 也可以写成
val dispatcher: CoroutineDispatcher? =
coroutineContext[ContinuationInterceptor] as? CoroutineDispatcher
-
这里使用
Job而非Job.Key是因为Job接口里有:public interface Job : CoroutineContext.Element { companion object Key : CoroutineContext.Key<Job> }companion object Key允许简写coroutineContext[Job]。 -
想调用调度器特有方法时:
val ioDispatcher = (coroutineContext[ContinuationInterceptor] as? CoroutineDispatcher)?.limitedParallelism(3)
五、删除某个上下文元素
val noJobContext = coroutineContext.minusKey(Job)
自定义 CoroutineContext 元素
- 继承自
AbstractCoroutineContextElement并提供唯一的Key:
class MyCoroutineContext : AbstractCoroutineContextElement(MyCoroutineContext) {
companion object Key : CoroutineContext.Key<MyCoroutineContext>
suspend fun log() {
println("Current coroutine context = $coroutineContext")
}
}
// 使用时:
val ctx = MyCoroutineContext()
val result = withContext(ctx) {
// 这里可以通过 coroutineContext[MyCoroutineContext] 拿到 ctx
coroutineContext[MyCoroutineContext]?.log()
// …
}
这样,你就可以在任意挂起函数或协程构建器里,通过 coroutineContext[MyCoroutineContext] 访问并使用你的自定义上下文元素。
学后测验
一、单项选择(每题 1 分,共 5 分)
-
CoroutineScope构造函数在你未传入Job时,会自动为其coroutineContext添加什么?
A.EmptyCoroutineContextB.SupervisorJobC. 新建的普通JobD.Dispatchers.Default
答案:C
解析: 源码CoroutineScope(context)会在 context 无 Job 时自动+ Job()。 -
GlobalScope.launch { … }内部默认使用的调度器是:
A.Dispatchers.MainB. 调用处的coroutineContextC.Dispatchers.DefaultD. 不确定,取决于平台
答案:C -
下面哪个写法会替换原协程中的 Dispatcher?
A.withContext(EmptyCoroutineContext) { … }
B.withContext(coroutineContext) { … }
C.withContext(Dispatchers.IO) { … }
D.coroutineScope { … }
答案:C
解析: 只有withContext且传入不同 Dispatcher 才会切换;其余沿用旧上下文。 -
对同一协程,若依次执行
coroutineContext + Dispatchers.IO + CoroutineName("A")最外层(最容易被
get()取得)的元素一定是:
A.JobB.CoroutineNameC.ContinuationInterceptorD. 插入顺序决定
答案:C
解析:plus内部保证ContinuationInterceptor(即 Dispatcher)放最外,检索更快。 -
关于
CoroutineName,以下说法正确的是:
A. 只在调试时可见,对运行时行为无影响 B. 会改变协程调度器名称 C. 同类型元素可叠加多个 D.CoroutineName("X") + CoroutineName("Y")会保留 “X”
答案:A
解析: B 错;C 错,同 Key 替换;D 错,保留最后一个 “Y”。
二、多项选择(每题 2 分,共 10 分)
- 下列哪些写法能在挂起函数里拿到当前协程的
Job?
A.coroutineContext[Job]B.currentCoroutineContext()[Job]C.coroutineContext.jobD.Job()
答案:A B C
解析: D 创建的是新 Job,不是当前的。 - 以下哪几种情况会导致
withContext不发生线程切换?
A. 传入EmptyCoroutineContext
B. 传入与当前上下文相同的 Dispatcher 但多了CoroutineName
C. 传入Dispatchers.IO而当前在Dispatchers.Default
D. 传入coroutineContext本身
答案:A B D - 关于
supervisorScope,以下说法正确的有:
A. 内部使用的是SupervisorJobB. 子协程抛异常不会取消兄弟协程 C. 父协程取消会继续保活兄弟协程 D. 自己执行完仍会向外层抛最后的异常
答案:A B D
**解析:**父协程取消仍会取消所有子协程(包括兄弟)。 CoroutineContext的+运算符做了哪些事?
A. 若右侧元素 Key 已存在,则替换左侧旧值
B. 始终把ContinuationInterceptor放在链表最外层
C. 遇到EmptyCoroutineContext直接返回另一侧
D. 把所有 Dispatcher 合并为一个复合 Dispatcher
答案:A B C- 以下关于自定义
CoroutineContext元素的必备条件有:
A. 必须实现CoroutineContext.ElementB. 必须声明伴生KeyC. 必须继承AbstractCoroutineContextElementD. 必须实现Job接口
答案:A B (或 C)
**解析:**A B 为必要;C 为推荐做法;D 非必需。
三、判断题(每题 1 分,共 5 分)
GlobalScope.coroutineContext[Job]返回null。 对coroutineScope { … }与withContext(EmptyCoroutineContext) { … }行为等价。 对limitedParallelism()可以对任何CoroutineDispatcher调用。 对- 在
coroutineScope中写lifecycleScope.async { … }生成的子协程仍受当前coroutineScope取消控制。 错 ContinuationInterceptor与CoroutineDispatcher是同一接口别名。 对
四、简答题(每题 5 分,共 20 分)
16. 简述 CoroutineContext 与 CoroutineScope 的职责区别,并说明为什么要分离设计。
答:
CoroutineContext:协程运行所需“元数据容器”,用键值形式保存Job、Dispatcher、CoroutineName、CoroutineExceptionHandler等元素。CoroutineScope:持有一份CoroutineContext,把它当“配置”去启动 (launch/async) 和取消 (cancel) 一组协程。
分离原因:
- 解耦复用——同一
CoroutineContext可被多个CoroutineScope共享或叠加组合,灵活切换调度器/Job。 - 单一职责——Scope 只关心生命周期管理,Context 专注存放元信息;代码层次清晰。
- 结构化并发——取消 Scope ⇒ 级联取消其 Context 中的根
Job及所有子协程,易于统一回收。
17. 说明 GlobalScope.launch 生成的协程在父子关系、取消传播上的特点,并列举一个合理使用场景。
答:
-
父子关系:
GlobalScope自身没有Job,所以GlobalScope.launch创建的协程 Job 的parent为null,它们各自是“根协程”。 -
取消传播:
- 不会跟随任何屏幕、页面或 ViewModel 的生命周期自动取消;
- 调用某个 Global 协程的
cancel()仅影响它自己,不会波及其他 Global 协程; - 进程被杀时才随之终结。
-
默认调度器:若未显式指定,会使用
Dispatchers.Default。 -
合理场景:真正进程级且“做完就好”的后台任务——例如应用生命周期内始终记录日志、定时上报指标、预生成数据库索引等。
18. 比较 coroutineScope、withContext、supervisorScope 三者在“挂起等待”“能否切换上下文”“异常传播”方面的差异。
| 比较点 | coroutineScope {} | withContext(ctx) {} | supervisorScope {} |
|---|---|---|---|
| 是否挂起调用方 | ✔ 挂起,直到内部协程全部结束 | ✔ 挂起,直到内部逻辑结束 | ✔ 挂起,直到内部协程全部结束 |
| 能否切换上下文 | ✘ 沿用外部 CoroutineContext | ✔ 由参数 ctx 决定(可换 Dispatcher / Job 等) | ✘ 沿用外部,但内部根 Job 被替换为 SupervisorJob |
| 异常传播规则 | 任一子协程抛非 CancellationException ⇒ 取消所有兄弟 + 向外抛异常 | 与左侧相同(若切换线程不影响规则) | 某个子协程抛异常 ⇒ 仅取消自身,其余兄弟继续;完成后把最后一个异常向外抛 |
19. CoroutineContext 合并时为什么总把 ContinuationInterceptor (即 CoroutineDispatcher) 放在最外层?
答:
- 查询频率最高——协程恢复时几乎每次都要取 Dispatcher;放最外层可 O(1) 直接获取,省去逐层解包。
- 独立性强——Dispatcher 不依赖其他元素,置顶不改变其它键值的可见性与逻辑。
- 实现简洁——把开销最大的元素放最外,内部合并/替换逻辑更简单,也减少链式嵌套深度。
五、编程题(每题 5 分,共 10 分)
-
limitedDispatcher
编写函数suspend fun currentLimitedDispatcher(n: Int): CoroutineDispatcher返回 以当前协程调度器为基础、并发度限制为
n的新 Dispatcher。若当前协程没有 Dispatcher,要抛出IllegalStateException。给出核心代码。参考实现:
suspend fun currentLimitedDispatcher(n: Int): CoroutineDispatcher { val dispatcher = coroutineContext[ContinuationInterceptor] as? CoroutineDispatcher ?: throw IllegalStateException("No dispatcher in context") return dispatcher.limitedParallelism(n) } -
contextDebug
写一个扩展函数suspend fun CoroutineScope.printContextInfo(prefix: String = "")要求打印当前协程的
Job层级深度(父链长度)与 Dispatcher 类型名称,例如>> depth=2 , dispatcher=DefaultDispatcher参考思路:
suspend fun CoroutineScope.printContextInfo(prefix: String = "") { val job = coroutineContext[Job] var depth = 0 var p = job?.parent while (p != null) { depth++; p = p.parent } val dispatcher = coroutineContext[ContinuationInterceptor] as? CoroutineDispatcher println("$prefix depth=$depth , dispatcher=${dispatcher?.javaClass?.simpleName}") }