深入理解 Kotlin 协程 (七):画地为营,解构协程作用域与父子羁绊

什么是协程作用域?

说到,你可能会想到数学中函数的定义域、值域,它通常用来描述一个范围。

而在 Kotlin 中,我们所说的协程作用域(CoroutineScope),主要是用来明确协程之间的父子关系,让某种行为(如取消操作、异常处理)能够在作用域中进行传播。

作用域主要有以下三种:

  • 顶级作用域: 没有父协程的协程所在的作用域,比如使用 GlobalScope 启动的协程所在的作用域。
  • 协同作用域: 协程中启动的子协程默认所在的作用域。在这个作用域下,子协程抛出的未捕获异常 都会上交给父协程处理,同时父协程会被取消。父子协程互相配合共同完成任务(协同),可以说是"荣辱与共"。
  • 主从作用域: 与协同作用域的父子关系一致,唯一区别在于,该作用域中的子协程出现未捕获的异常时,不会将异常向上传递给父协程。子协程的崩溃,不会影响到父协程及其兄弟协程的正常执行。

只要建立了父子关系,协程之间就必须遵守以下规则:

  • 父协程被取消时,其所有子协程也会被取消。
  • 父协程需要等待其所有子协程执行完成后,才会最终进入完成状态,无论父协程自身是否已经执行完毕。
  • 子协程会继承父协程的协程上下文元素。如果在启动子协程时,传入了相同 key 的元素,会覆盖父协程对应的元素。这种覆盖效果不会影响到父协程及兄弟协程,但会被自身的子协程继承。

为协程构建器注入作用域

我们先来看看,协程作用域的通用接口:

kotlin 复制代码
// [CoroutineScope.kt]
interface CoroutineScope {
    // 作用域上下文
    val scopeContext: CoroutineContext
}

为了方便管理协程的生命周期,我们将之前的 launchasync 协程构建器变为 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
}

同时,我们让 AbstractCoroutineStandaloneCoroutineDeferredCoroutine 的父类)来实现 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)
}

有了它,异常向上传递的路径就被"截断"了。

协程完整的异常处理流程

看完源码后,我们再回顾一下整个协程异常的流转逻辑:

flowchart TD A([开始]) --> B("尝试处理异常\n(tryHandleException)") B --> C{是否为取消异常?} %% cancel分支 C -->|是| D[返回false,取消属于正常行为,\n不进行异常上报] D --> Z([结束]) %% not cancel分支 C -->|不是| E{"父协程是否为主管协程\n(SupervisorCoroutine)或为空"} %% 递归向上传递异常 E -->|不是| F["交由父协程处理该异常\n(parentJob.handleChildException)"] F --> |触发父级的\n异常处理流程| B %% 阻断向上传递,被迫自行处理 E -->|是| G["只好自行处理异常\n(handleJobException)"] G --> H{当前协程的类型是?} %% DeferredCoroutine分支 H -->|"延迟协程(async启动)"| I["暂时存下异常,直到调用await()时抛出"] I --> J(( )) %% StandaloneCoroutine分支 H -->|"独立协程(launch启动)"| K{"协程异常处理器\n(CoroutineExceptionHandler)\n是否配置?"} K -->|否| L["由线程未捕获异常处理器\n(Thread.uncaughtExceptionHandler)\n兜底崩溃"] K -->|是| M[交由异常处理器执行自定义逻辑] L --> N(( )) M --> N(( )) N --> O[处理完毕] O --> J(( )) J --> Z([结束])

注意:虽然 async 启动的 DeferredCoroutine 的未捕获异常只有在调用 await() 时才抛出,但完全不影响它第一时间将异常向上传递给父协程。

父协程对子协程的等待

最后是规则二:"父协程需要等待所有子协程完成后才能进入完成状态"。

其实就是父协程的代码块执行到末尾时,对自身的子协程集合进行检查。如果还有子协程在运行中,父协程就会处于 WaitChildren 挂起状态。它会监听这些运行中的子协程的完成状态,只有当最后一个子协程完成后,父协程才会将自己的状态流转为 Complete,并触发完成回调。

代码实现:

  1. 增加 WaitChildren 状态,它需要暂存父协程的执行结果。
kotlin 复制代码
// [CoroutineState.kt]
class WaitChildren<T>(val value: T? = null, val exception: Throwable? = null) : CoroutineState()
  1. 我们要在 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()
    }
}
  1. 修改之前的状态流转逻辑:

父协程的代码块执行完毕时:

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()
    }
}
  1. 最后实现最后一个子协程执行完毕后,父协程的状态流转逻辑:
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查阅源代码。

相关推荐
xinhuanjieyi8 小时前
Android 画板应用kotlin实现
android·开发语言·kotlin
Coffeeee8 小时前
准备升级到Android16,自适应布局应该如何适配
android·google·kotlin
plainGeekDev9 小时前
ContentProvider → Room + Repository
android·java·kotlin
plainGeekDev9 小时前
SQLite 手动升级 → Room Migration
android·java·kotlin
消失的旧时光-19439 小时前
Kotlin 协程设计思想(十):Kotlin 协程到底解决了什么问题?
开发语言·kotlin·生命周期·rxjava·协程·结构化并发
Kapaseker10 小时前
Kotlin 集合:只读不等于不可变
android·kotlin
黄林晴10 小时前
绝了!Compose Multiplatform 也能实现 iOS26 液态玻璃的效果了
android·kotlin
JohnnyDeng941 天前
【Android】Room 数据库高级用法与性能调优:从查询瓶颈到毫秒级响应
android·性能优化·kotlin·room
Refrain_zc1 天前
Android 英语口语评测:从录音采集到单词级着色反馈的完整技术方案
kotlin