什么是协程作用域?
说到域,你可能会想到数学中函数的定义域、值域,它通常用来描述一个范围。
而在 Kotlin 中,我们所说的协程作用域(CoroutineScope),主要是用来明确协程之间的父子关系,让某种行为(如取消操作、异常处理)能够在作用域中进行传播。
作用域主要有以下三种:
- 顶级作用域: 没有父协程的协程所在的作用域,比如使用
GlobalScope启动的协程所在的作用域。 - 协同作用域: 协程中启动的子协程默认所在的作用域。在这个作用域下,子协程抛出的未捕获异常 都会上交给父协程处理,同时父协程会被取消。父子协程互相配合共同完成任务(协同),可以说是"荣辱与共"。
- 主从作用域: 与协同作用域的父子关系一致,唯一区别在于,该作用域中的子协程出现未捕获的异常时,不会将异常向上传递给父协程。子协程的崩溃,不会影响到父协程及其兄弟协程的正常执行。
只要建立了父子关系,协程之间就必须遵守以下规则:
- 父协程被取消时,其所有子协程也会被取消。
- 父协程需要等待其所有子协程执行完成后,才会最终进入完成状态,无论父协程自身是否已经执行完毕。
- 子协程会继承父协程的协程上下文元素。如果在启动子协程时,传入了相同 key 的元素,会覆盖父协程对应的元素。这种覆盖效果不会影响到父协程及兄弟协程,但会被自身的子协程继承。
为协程构建器注入作用域
我们先来看看,协程作用域的通用接口:
kotlin
// [CoroutineScope.kt]
interface CoroutineScope {
// 作用域上下文
val scopeContext: CoroutineContext
}
为了方便管理协程的生命周期,我们将之前的 launch 和 async 协程构建器变为 CoroutineScope 的扩展函数:
kotlin
// [Builders.kt]
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> Unit
): Job {
val completion = StandaloneCoroutine(context = newCoroutineContext(context))
block.startCoroutine(receiver = completion, completion = completion)
return completion
}
fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
val completion = DeferredCoroutine<T>(context = newCoroutineContext(context))
block.startCoroutine(receiver = completion, completion = completion)
return completion
}
fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
// 为即将创建的协程组合作用域上下文
val combined = scopeContext + context
// 如果不存在拦截器或未指定为Default调度器,则追加Default调度器
return if (combined !== Dispatchers.Default
&& combined[ContinuationInterceptor] == null
) {
combined + Dispatchers.Default
} else combined
}
同时,我们让 AbstractCoroutine(StandaloneCoroutine 和 DeferredCoroutine 的父类)来实现 CoroutineScope 接口:
kotlin
// [AbstractCoroutine.kt]
abstract class AbstractCoroutine<T>(context: CoroutineContext) : Job, Continuation<T>, CoroutineScope {
// ...
override val scopeContext: CoroutineContext
get() = context
}
这样,每次新创建协程时,它的上下文就是父级的作用域上下文 与本次传入的上下文 的组合(combined = scopeContext + context),这也是为什么协程上下文能像树的根系一样传递的原因。
建立协程间的父子关系
针对前面的规则一:"父协程取消后,子协程也要取消",我们是这么建立父子关系的:
kotlin
// [AbstractCoroutine.kt]
// 注意:该 context 是构造函数中外部传入的协程上下文,即前面的组合上下文
// 此时还未将自身实例作为元素加入,所以获取到的是父协程实例
protected val parentJob = context[Job]
private var parentCancelDisposable: Disposable? = null
init {
// 通过协程上下文获取父协程,并注册其取消回调
// 确保在父协程取消时,子协程也立即调用 cancel() 进入取消状态
parentCancelDisposable = parentJob?.invokeOnCancel {
cancel()
}
}
如果上下文中不存在父协程,说明当前协程处于顶级作用域中,是"孤儿协程",自然无需注册这个取消回调。
脱离束缚的顶级作用域
既然现在创建协程必须要有作用域,而作用域又是在创建协程时才产生的。这似乎陷入到了一个"死结",那么我们该如何创建一个协程呢?
很简单,只需我们提供一个初始"入口",也就是实现一个空上下文的作用域:
kotlin
// [GlobalScope.kt]
object GlobalScope : CoroutineScope {
override val scopeContext: CoroutineContext
get() = EmptyCoroutineContext
}
通过 GlobalScope 启动的协程就是根协程,而该协程的 Receiver 就是一个作用域实例,所以我们可以在其内部创建新的子协程,从而得到一颗"协程树":
kotlin
GlobalScope.launch { // ...... 1 (根协程)
launch { // ...... 2 (1 是 2 的父协程)
}
launch { // ...... 3 (1 是 3 的父协程)
launch { // ...... 4 (3 是 4 的父协程)
}
}
}
但如果你在协程内部使用 GlobalScope 来创建新协程,那么它们之间就不会产生父子关系,两个都是独立的根协程:
kotlin
GlobalScope.launch { // ...... 1
GlobalScope.launch { // ...... 2 (与 1 无父子关系)
}
}
无显式作用域环境下的协程创建
在实际开发中,根协程通常由框架帮我们创建好,我们大多数都是在挂起函数中写逻辑。
如果挂起函数本身提供了协程作用域作为 Receiver,我们当然可以直接调用 launch 创建子协程;但如果这是一个普通的挂起函数呢?
比如:
kotlin
suspend fun noScope() {
// 此时无法直接调用 launch
}
不用怕,其实这和在挂起函数中如何获取当前协程 类似,你只需记住关键的一点:当前挂起函数一定运行在某个协程之中,只是框架将其隐藏了。
既然作用域的本质只是对协程上下文的封装,我们完全可以调用 suspendCoroutine 拿到当前挂起函数底层的 Continuation 实例,然后获取它的上下文,现场构造一个作用域出来。
这也正是标准库中 coroutineScope 构建器的逻辑:
kotlin
// [Builders.kt]
suspend fun <R> coroutineScope(
block: suspend CoroutineScope.() -> R
): R = suspendCoroutine { continuation ->
val coroutine = ScopeCoroutine(
context = continuation.context,
continuation = continuation
)
block.startCoroutine(receiver = coroutine, completion = coroutine)
}
// 作用域的包装类
internal open class ScopeCoroutine<T>(
context: CoroutineContext,
protected val continuation: Continuation<T>
) : AbstractCoroutine<T>(context) {
override fun resumeWith(result: Result<T>) {
super.resumeWith(result)
continuation.resumeWith(result)
}
}
这么一来,在 noScope 中获取作用域只需这样:
kotlin
suspend fun noScope() {
coroutineScope {
launch {
// 现在可以自由创建子协程
}
}
}
同理,在 suspend fun main() 中使用 coroutineScope,只是把 main 函数背后创建的那个简单协程当作了根节点,后续的代码都在 coroutineScope 捏造的作用域中执行。
kotlin
suspend fun main() {
coroutineScope {
launch {
}
async {
}
}
}
协同作用域的异常传递
在协程体内直接创建的新协程通常就是子协程,而它默认所在的作用域就是协同作用域。
我们来梳理一下协同作用域中"子协程异常交由父协程处理"的流程。
之前协程执行完成时,会将异常丢给 tryHandleException 去分发处理(其中取消异常不处理,其他异常交给 handleJobException)
kotlin
// [AbstractCoroutine.kt]
private fun tryHandleException(e: Throwable): Boolean {
return when (e) {
is CancellationException -> {
false
}
else -> {
handleJobException(e)
}
}
}
// [StandaloneCoroutine.kt]
// 对于 launch 启动的独立协程
override fun handleJobException(e: Throwable): Boolean {
super.handleJobException(e)
context[CoroutineExceptionHandler]?.handleException(context, e)
?: Thread.currentThread().let {
it.uncaughtExceptionHandler.uncaughtException(it, e)
}
return true
}
而按照协同作用域的规则,一旦出现未捕获的异常,必须先向上汇报,只有在没有父协程的情况下才自行处理。
为此,我们定义 handleChildException 函数,用于向上汇报:
kotlin
/**
* 由子协程调用,让父协程尝试处理异常
*/
// [AbstractCoroutine.kt]
protected open fun handleChildException(e: Throwable): Boolean {
// 让父协程取消自己(此时是父协程对象在执行)
cancel()
// 尝试处理子协程传递过来的异常
return tryHandleException(e)
}
接着,我们修改之前的异常分发逻辑,优先调用父节点的 handleChildException:
kotlin
// [AbstractCoroutine.kt]
private fun tryHandleException(e: Throwable): Boolean {
return when (e) {
is CancellationException -> {
false
}
else -> {
// 优先让父协程处理,如果父协程处理了,就直接返回
(parentJob as? AbstractCoroutine<*>)?.handleChildException(e)
?.takeIf { it }
?: handleJobException(e) // 无父协程,则自行处理
}
}
}
如果父协程还有父协程,这个异常就会沿着树形结构向上"冒泡"。这样解释了一个经典误区:在协同作用域中,给子协程单独设置 CoroutineExceptionHandler 是毫无意义的,因为子协程的异常处理逻辑一般不会被触发。
主从作用域
有些场景中,协同作用域的"一损俱损"的连带机制并不适用。例如一个多协程的并发下载器,每一个协程都在执行一个下载任务,我们不希望其中某一个任务的失败,导致界面中的其他请求被取消。
这时,就需要用到主从作用域(SupervisorScope),它的目标是父协程仍然可以向下控制(取消)子协程,但子协程崩溃时不能连累到父协程。
在代码实现上非常简单,只是重写向上汇报的入口(handleChildException)并直接返回 false 即可,这样就能屏蔽子协程抛上来的异常。
kotlin
private class SupervisorCoroutine<T>(
context: CoroutineContext,
continuation: Continuation<T>
) : ScopeCoroutine<T>(context, continuation) {
// 直接阻断了异常的向上传播
override fun handleChildException(e: Throwable): Boolean {
return false
}
}
创建这个防火墙作用域的函数如下:
kotlin
suspend fun <R> supervisorScope(
block: suspend CoroutineScope.() -> R
): R = suspendCoroutine { continuation ->
val coroutine = SupervisorCoroutine(
context = continuation.context,
continuation = continuation
)
block.startCoroutine(receiver = coroutine, completion = coroutine)
}
有了它,异常向上传递的路径就被"截断"了。
协程完整的异常处理流程
看完源码后,我们再回顾一下整个协程异常的流转逻辑:
注意:虽然
async启动的DeferredCoroutine的未捕获异常只有在调用await()时才抛出,但完全不影响它第一时间将异常向上传递给父协程。
父协程对子协程的等待
最后是规则二:"父协程需要等待所有子协程完成后才能进入完成状态"。
其实就是父协程的代码块执行到末尾时,对自身的子协程集合进行检查。如果还有子协程在运行中,父协程就会处于 WaitChildren 挂起状态。它会监听这些运行中的子协程的完成状态,只有当最后一个子协程完成后,父协程才会将自己的状态流转为 Complete,并触发完成回调。
代码实现:
- 增加
WaitChildren状态,它需要暂存父协程的执行结果。
kotlin
// [CoroutineState.kt]
class WaitChildren<T>(val value: T? = null, val exception: Throwable? = null) : CoroutineState()
- 我们要在
AbstractCoroutine中使用原子计数器记录存活子协程的个数,并且每一个子协程被创建时,都要去父协程中"报道"(计数器加一),在子协程完成(invokeOnCompletion)时,父协程的计数器减一。
kotlin
// [AbstractCoroutine.kt]
// 记录当前依然活跃的子协程数量
private val activeChildrenCount = AtomicInteger(0)
init {
// 建立取消的向下传播链
parentCancelDisposable = parentJob?.invokeOnCancel {
cancel()
}
// 建立生命的向上传播链
(parentJob as? AbstractCoroutine<*>)?.addChild(this@AbstractCoroutine)
}
/**
* 由子协程调用,向父协程注册自己
*/
fun addChild(child: Job) {
activeChildrenCount.incrementAndGet()
// 当子协程完成时,通知父协程计数减少
child.invokeOnCompletion {
onChildComplete()
}
}
private fun onChildComplete() {
// 最后一个子协程完成
if (activeChildrenCount.decrementAndGet() == 0) {
tryCompleteAfterChildren()
}
}
- 修改之前的状态流转逻辑:
父协程的代码块执行完毕时:
kotlin
// [AbstractCoroutine.kt]
override fun resumeWith(result: Result<T>) {
val value = result.getOrNull()
val exception = result.exceptionOrNull()
val newState = state.updateAndFetch { prevState ->
when (prevState) {
is CoroutineState.Cancelling, is CoroutineState.Incomplete -> {
if (activeChildrenCount.get() > 0) {
// 还有子协程存活
CoroutineState.WaitChildren(value, exception).from(prevState)
} else {
// 没有子协程或是都完成了,直接进入完成状态
CoroutineState.Complete(value, exception).from(prevState)
}
}
is CoroutineState.WaitChildren<*>, is CoroutineState.Complete<*> -> {
throw IllegalStateException("Already completed!")
}
}
}
(newState as? CoroutineState.Complete<*>)?.exception?.let { e ->
tryHandleException(e = e)
}
// 只有在进入 Complete 状态时,才去触发外部的完成回调
if (newState is CoroutineState.Complete<*>) {
newState.let {
it.notifyCompletion(result)
it.clear()
}
}
}
父协程注册完成回调时:
kotlin
// [AbstractCoroutine.kt]
protected fun doOnCompleted(block: (Result<T>) -> Unit): Disposable {
val disposable = CompletionHandlerDisposable(this, block)
val newState = state.updateAndFetch { prev ->
when (prev) {
is CoroutineState.Incomplete -> {
CoroutineState.Incomplete().from(prev).with(disposable)
}
is CoroutineState.Cancelling -> {
CoroutineState.Cancelling().from(prev).with(disposable)
}
is CoroutineState.WaitChildren<*> -> {
// 还未完成,同样需要注册完成回调并保留已暂存的结果
CoroutineState.WaitChildren(prev.value, prev.exception).with(disposable)
}
// 如果已经是完成状态,则保持不变
is CoroutineState.Complete<*> -> prev
}
}
(newState as? CoroutineState.Complete<T>)?.let {
val result = when {
it.value != null -> Result.success(it.value)
it.exception != null -> Result.failure(it.exception)
else -> throw IllegalStateException("Won't happen.")
}
block(result)
}
return disposable
}
父协程移除完成回调时:
kotlin
// [AbstractCoroutine.kt]
override fun remove(disposable: Disposable) {
state.update { prev ->
return@update when (prev) {
is CoroutineState.Incomplete -> {
CoroutineState.Incomplete().from(prev).without(disposable)
}
is CoroutineState.Cancelling -> {
CoroutineState.Cancelling().from(prev).without(disposable)
}
is CoroutineState.WaitChildren<*> -> {
// 还未完成,移除已注册的完成回调
CoroutineState.WaitChildren(prev.value, prev.exception).from(prev).without(disposable)
}
is CoroutineState.Complete<*> -> {
// 已经完成,无需处理
prev
}
}
}
}
外部协程等待父协程时:
kotlin
// [AbstractCoroutine.kt]
override suspend fun join() {
when (state.load()) {
is CoroutineState.Incomplete,
is CoroutineState.Cancelling,
is CoroutineState.WaitChildren<*> -> {
// 被等待的协程尚未完成,挂起等待
return joinSuspend()
}
is CoroutineState.Complete<*> -> {
val currentCancelling = currentCoroutineContext()[Job]?.isActive ?: return
if (!currentCancelling) {
throw CancellationException("Coroutine is already cancelled")
}
return
}
}
}
kotlin
// [DeferredCoroutine.kt]
override suspend fun await(): T {
return when (val currentState = state.load()) {
is CoroutineState.Incomplete,
// 等待子协程完成,如果认为结果已就绪直接返回,那么之后可能会因为子协程的崩溃从而破坏结构化并发
is CoroutineState.WaitChildren<*>,
is CoroutineState.Cancelling -> awaitSuspend()
is CoroutineState.Complete<*> -> {
// 如果已完成且有异常,直接抛出;否则返回正常结果
currentState.exception?.let { throw it }
?: (currentState.value as T)
}
}
}
父协程注册取消回调时:
kotlin
// [AbstractCoroutine.kt]
override fun invokeOnCancel(onCancel: OnCancel): Disposable {
val disposable = CancellationHandleDisposable(job = this@AbstractCoroutine, onCancel = onCancel)
val newState = state.updateAndFetch { prev ->
when (prev) {
is CoroutineState.Incomplete -> {
// 新增回调
CoroutineState.Incomplete().from(state = prev).with(disposable = disposable)
}
is CoroutineState.Cancelling,
is CoroutineState.WaitChildren<*>, // 协程体已执行完,此时注册取消回调无意义
is CoroutineState.Complete<*> -> {
prev
}
}
}
(newState as? CoroutineState.Cancelling)?.let {
onCancel()
}
return disposable
}
父协程被取消时:
kotlin
// [AbstractCoroutine.kt]
override fun cancel() {
val prevState = state.fetchAndUpdate { prev ->
when (prev) {
is CoroutineState.Cancelling,
is CoroutineState.WaitChildren<*>, // 协程体已完成完毕,只是子协程还未完成,所以应该保持不变,否则这时取消会传播给子协程
is CoroutineState.Complete<*> -> {
prev
}
is CoroutineState.Incomplete -> {
CoroutineState.Cancelling().from(prev)
}
}
}
if (prevState is CoroutineState.Incomplete) {
prevState.notifyCancellation()
}
}
- 最后实现最后一个子协程执行完毕后,父协程的状态流转逻辑:
kotlin
// [AbstractCoroutine.kt]
private fun tryCompleteAfterChildren() {
val newState = state.updateAndFetch { prevState ->
when (prevState) {
// 只有处于等待状态的协程,才能被子协程的完成所触发流转
is CoroutineState.WaitChildren<*> -> {
// 获取之前暂存的返回值和异常
CoroutineState.Complete(prevState.value, prevState.exception).from(prevState)
}
// 其他状态,无需处理
else -> prevState
}
}
if (newState is CoroutineState.Complete<*>) {
// 通知外部 join/await 的协程完成
val result = when {
newState.exception != null -> Result.failure(newState.exception)
else -> Result.success(value = newState.value)
}
newState.notifyCompletion(result)
newState.clear()
}
}
博客中的源码难免有误,可到CoroutineLite查阅源代码。