深入理解 Kotlin 协程 (六):进退有度,解密协程取消响应与异常分发机制

协程的取消机制

取消协程需要协程内部配合,这点和线程一样,本质上也是协作式的取消,就是将状态设置为取消,协程内部根据状态的变化来响应。

完善 Job 的状态流转与取消通知

我们基于上一篇博客中的代码,来完善协程的取消逻辑。

首先支持协程取消回调的注册:

kotlin 复制代码
// [AbstractCoroutine.kt]
override fun invokeOnCancel(onCancel: OnCancel): Disposable {
    // 1. 创建回调包装对象,以便后续可以手动解绑
    val disposable = CancellationHandleDisposable(job = this@AbstractCoroutine, onCancel = onCancel)
    // 2. 原子更新协程状态
    val newState = state.fetchAndUpdate { prev ->
        when (prev) {
            is CoroutineState.Incomplete -> {
                // 如果协程还在运行中,就追加回调
                CoroutineState.Incomplete().from(state = prev).with(disposable = disposable)
            }

            is CoroutineState.Cancelling,
            is CoroutineState.Complete<*> -> {
                // 已取消或已完成,无需注册(无意义),保持当前状态
                prev
            }
        }
    }

    // 3. 如果注册时正在取消,立即同步触发回调
    (newState as? CoroutineState.Cancelling)?.let {
        onCancel()
    }

    return disposable
}

// [CancellationHandleDisposable.kt]
/**
 * 可移除的取消回调
 */
class CancellationHandleDisposable(
    val job: Job,
    val onCancel: OnCancel
) : Disposable {
    override fun dispose() {
        // 从当前绑定的协程实例中移除自身
        job.remove(disposable = this@CancellationHandleDisposable)
    }
}

接着是 cancel 函数的实现:

kotlin 复制代码
// [AbstractCoroutine.kt]
override fun cancel() {
    // 1. 原子流转状态
    val prevState = state.fetchAndUpdate { prev ->
        when (prev) {
            is CoroutineState.Cancelling,
            is CoroutineState.Complete<*> -> {
                // 保持不变,意味着重复调用 cancel 无任何副作用
                prev
            }

            is CoroutineState.Incomplete -> {
                // 流转为取消状态
                CoroutineState.Cancelling().from(prev)
            }
        }
    }

    // 2. 只有在取消之前是未完成状态,才去通知注册的取消回调
    if (prevState is CoroutineState.Incomplete) {
        prevState.notifyCancellation()
    }
}

// [CoroutineState.kt]
/**
 * 遍历并触发所有已注册的取消回调
 */
fun notifyCancellation() {
    this@CoroutineState.disposableList.loopOn<CancellationHandleDisposable> {
        it.onCancel()
    }
}

注意,这里并不能这样实现:

kotlin 复制代码
// 先更新后获取新状态
val newState = state.updateAndFetch {
    // ...
}

if (newState is CoroutineState.Cancelling) {
    // 旧状态可能是 Incomplete 或 Cancelling
    // 但我们只要从 Incomplete 转变为 Cancelling 的情况
    prevState.notifyCancellation()
}

因为 cancel 允许多次调用,在协程第一次调用 cancel 到结束之前,每次调用 cancel 得到的新状态都会是 Cancelling,这就会导致后续的 if 语句一定为 true,存在取消回调被多次重复通知的情况。

就算你在通知后将回调列表清空,也可能会出现这种情况:协程 A 通知后、清空前,协程 B 又进行了并发通知,还是无法避免,因为通知回调和清空回调就不是原子性的。

挂起函数响应取消的底层支撑:CancellableContinuation

怎么让挂起函数支持取消呢?其实我们之前提到过了,就是让它能够检查当前取消状态,并在取消时停止耗时任务。

参考标准库中的 suspendCoroutine 函数来实现 suspendCancellableCoroutine 函数:

kotlin 复制代码
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
import kotlin.coroutines.intrinsics.intercepted

// [CancellableContinuation.kt]
suspend inline fun <T> suspendCancellableCoroutine(
    crossinline block: (CancellableContinuation<T>) -> Unit
) = suspendCoroutineUninterceptedOrReturn { continuation: Continuation<T> ->
    // 拦截原始的 continuation,并用 CancellableContinuation 包装以接管挂起与取消逻辑
    val cancellable = CancellableContinuation(continuation.intercepted())
    block(cancellable)
    cancellable.getResult()
}

suspendCoroutineUninterceptedOrReturn 我们早已见过,调用它能够获取到一个未被拦截(Unintercepted)的原始 Continuation 实例,在其 Lambda 中我们需要返回(OrReturn)一个值:如果结果已经准备好了(快路径),我们直接返回结果,否则(慢路径)返回一个特殊的编译器标记常量 COROUTINE_SUSPENDED

其中的 CancellableContinuation 应该能够注册取消回调(invokeOnCancellation)、能够监听取消状态。

首先来定义状态:

kotlin 复制代码
// [CancellableContinuation.kt]
/**
 * 挂起点内部状态
 */
sealed class CancelState {
    object Incomplete : CancelState()
    class CancelHandler(val onCancel: OnCancel) : CancelState() // 只允许注册一个取消回调
    class Complete<T>(
        val value: T? = null,
        val exception: Throwable? = null
    ) : CancelState()

    object Cancelled : CancelState()
}

// 挂起决策
enum class CancelDecision {
    UNDECIDED, // 初始状态:还未决定
    SUSPENDED, // 确定挂起 
    RESUMED // 确定已拿到结果
}

CancellableContinuation 的实现只需静态代理 Continuation 即可:在完成(resumeWith)时流转状态,支持取消回调的注册。

kotlin 复制代码
import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED

// [CancellableContinuation.kt]
@OptIn(ExperimentalAtomicApi::class)
class CancellableContinuation<T>(
    private val continuation: Continuation<T>
) : Continuation<T> by continuation {
    private val state = AtomicReference<CancelState>(CancelState.Incomplete)
    private val decision = AtomicReference(CancelDecision.UNDECIDED)

    // 是否完成
    val isCompleted: Boolean
        get() = when (state.load()) {
            CancelState.Incomplete,
            is CancelState.CancelHandler -> false

            is CancelState.Complete<*>,
            CancelState.Cancelled -> true
        }

    /**
     * 注册取消回调
     */
    fun invokeOnCancellation(onCancel: OnCancel) {
        val newState = state.updateAndFetch { prev ->
            when (prev) {
                CancelState.Incomplete -> CancelState.CancelHandler(onCancel)
                is CancelState.CancelHandler ->
                    throw IllegalStateException("Prohibited.")

                is CancelState.Complete<*>,
                CancelState.Cancelled -> prev
            }
        }
        if (newState is CancelState.Cancelled) {
            onCancel()
        }
    }

    /**
     * 绑定父协程,随着父协程的取消而取消
     */
    private fun installCancelHandler() {
        if (isCompleted) return
        val parent = continuation.context[Job] ?: return
        parent.invokeOnCancel {
            doCancel()
        }
    }

    private fun doCancel() {
        val prevState = state.fetchAndUpdate { prev ->
            when (prev) {
                is CancelState.CancelHandler,
                CancelState.Incomplete -> {
                    // 流转为取消状态
                    CancelState.Cancelled
                }

                CancelState.Cancelled,
                is CancelState.Complete<*> -> {
                    prev
                }
            }
        }
        if (prevState is CancelState.CancelHandler) {
            // 执行取消回调
            prevState.onCancel()
            resumeWithException(CancellationException("Cancelled.")) // 抛出异常响应取消
        }
    }

    /**
     * 决定是真正挂起,还是同步返回结果
     */
    @Suppress("UNCHECKED_CAST")
    fun getResult(): Any? {
        // 此时才绑定,为的是在真正的挂起点注册
        installCancelHandler()
        if (decision.compareAndSet(CancelDecision.UNDECIDED, CancelDecision.SUSPENDED))
            // 结果尚未就绪,返回挂起标志
            return COROUTINE_SUSPENDED

        // 此时没有真正挂起(decision 为 RESUMED),同步完成,可以获取结果
        return when (val currentState = state.load()) {
            is CancelState.CancelHandler,
            CancelState.Incomplete -> COROUTINE_SUSPENDED

            CancelState.Cancelled ->
                throw CancellationException("Continuation is cancelled.")

            is CancelState.Complete<*> -> {
                (currentState as CancelState.Complete<T>).let {
                    it.exception?.let { e -> throw e } ?: it.value
                }
            }
        }
    }

    /**
     * 完成回调
     */
    override fun resumeWith(result: Result<T>) {
        when {
            // 同步完成:抢先修改决策,将结果存入状态
            decision.compareAndSet(CancelDecision.UNDECIDED, CancelDecision.RESUMED) -> {
                state.store(
                    CancelState.Complete(
                        result.getOrNull(),
                        result.exceptionOrNull()
                    )
                )
            }
            
            // 异步恢复:当前已挂起,直接唤醒底层的 continuation
            decision.compareAndSet(CancelDecision.SUSPENDED, CancelDecision.RESUMED) -> {
                state.updateAndFetch { prev ->
                    when (prev) {
                        is CancelState.Complete<*> -> {
                            throw IllegalStateException("Already completed.")
                        }

                        else -> {
                            CancelState.Complete(
                                result.getOrNull(),
                                result.exceptionOrNull()
                            )
                        }
                    }
                }
                continuation.resumeWith(result)
            }
        }
    }
}

getResult()resumeWith() 处理了协程底层的并发竞态问题,尝试挂起(getResult) 与任务完成尝试唤醒 (resumeWith),需要进行"赛跑":

  • 最常见的情况是慢路径(真正挂起),此时异步任务耗时较长,getResult() 会先执行,将 decision 置为 SUSPEND,协程会交出线程的执行权进行挂起。当异步任务完成后,触发 resumeWith,发现协程挂起,就会调用底层的 continuation.resumeWith 将其唤醒。

  • 如果异步任务很快就完成,此时 getResult() 还没来得及执行,resumeWith 会将状态改为 RESUME 并将结果放在状态中,此时协程还没有挂起。接着 getResult() 会尝试挂起,发现此时已经得到了结果,就会取出结果同步返回,不会挂起和切线程。

改造具体挂起函数以支持取消

有了 CancellableContinuation 后,我们就可以让挂起函数感知到取消了。

改造 delay

首先是 delay,添加取消响应:

kotlin 复制代码
suspend fun delay(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS) {
    if (time <= 0) {
        return
    }

    suspendCancellableCoroutine { continuation ->
        val future = executor.schedule({ continuation.resume(Unit) }, time, unit)
        continuation.invokeOnCancellation {
            // 当所在协程被取消时,取消底层的 future 定时任务
            future.cancel(true)
        }
    }
}

改造 join

接着改造 join,响应取消时,暂时不恢复对应的协程,保持挂起状态。

KOTLIN 复制代码
private suspend fun joinSuspend() = suspendCancellableCoroutine { continuation ->
    val disposable = doOnCompleted { _ ->
        continuation.resume(value = Unit)
    }
    continuation.invokeOnCancellation {
        // 所在协程被取消时,移除需要等待的目标协程注册的完成回调
        disposable.dispose()
    }
}

如果 join 函数对应的协程已经完成,我们可以直接返回;但此时,我们可以检查一下当前所在协程的状态,如果已经取消,则抛出取消异常予以响应。

kotlin 复制代码
override suspend fun join() {
    when (state.load()) {
        is CoroutineState.Incomplete,
        is CoroutineState.Cancelling -> {
            // 被等待的协程尚未完成,挂起等待
            return joinSuspend()
        }

        is CoroutineState.Complete<*> -> {
            // 目标协程已完成,但需主动检查当前所在协程的活跃状态以响应取消
            val isActive = currentCoroutineContext()[Job]?.isActive ?: return
            if (!isActive) {
                throw CancellationException("Coroutine is already cancelled")
            }
            return
        }
    }
}

协程的异常处理机制

我们接着来讲清楚协程中的异常应该怎么处理:

  • 在协程体内,无论是挂起函数还是普通函数抛出的异常,都可以通过 try...catch 来捕获。
  • 对于未捕获的异常,则在获取结果时处理。

定义与简化异常处理器

为了统一拦截和处理第二种未捕获异常,我们来定义一个异常处理器。

kotlin 复制代码
interface CoroutineExceptionHandler : CoroutineContext.Element {
    companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>

    /**
     * 处理异常
     */
    fun handleException(context: CoroutineContext, exception: Throwable)
}

然后创建一个函数,来简化它的创建:

kotlin 复制代码
inline fun CoroutineExceptionHandler(
    crossinline handler: (CoroutineContext, Throwable) -> Unit
): CoroutineExceptionHandler {
    return object : AbstractCoroutineContextElement(key = CoroutineExceptionHandler), CoroutineExceptionHandler {
        override fun handleException(context: CoroutineContext, exception: Throwable) {
            handler.invoke(context, exception)
        }
    }
}

未捕获异常的分发与处理

我们在 AbstractCoroutine 定义一个 handleJobException 函数,返回 true 表示异常已处理。它的子类可以根据自身需要它来实现特有的异常处理逻辑。

kotlin 复制代码
/**
 * 处理未捕获异常
 */
protected open fun handleJobException(e: Throwable) = false

它的子类有两个,异常处理逻辑有些不同:

  • StandaloneCoroutine:launch 启动,自身无返回结果。我们希望它在遇到未捕获的异常时,优先调用自身的异常处理器进行处理,如果没有进行配置,则将异常抛给 completion 调用时所在线程的 uncaughtExceptionHandler 来兜底。

  • DeferredCoroutine:async 启动,通常有返回结果。由于它需要将结果返回给调用者,所以我们无需覆写该方法去主动处理异常,而是将未捕获的异常存放在最终状态中,等外部调用 await 获取结果时,才将异常抛出。

StandaloneCoroutine 的具体实现如下:

kotlin 复制代码
// [StandaloneCoroutine.kt]
override fun handleJobException(e: Throwable): Boolean {
    super.handleJobException(e)
    // 优先由自身的异常处理器处理,无配置则由线程的 uncaughtExceptionHandler 兜底
    context[CoroutineExceptionHandler]?.handleException(context, e)
        ?: Thread.currentThread().let {
            it.uncaughtExceptionHandler.uncaughtException(it, e)
        }
    return true
}

处理的逻辑完成后,在协程执行完成处进行异常的尝试分发:

kotlin 复制代码
override fun resumeWith(result: Result<T>) {
    // ...
        
    // 若最终状态携带异常,进入异常分发流程
    (newState as? CoroutineState.Complete<T>)?.exception?.let { e ->
        tryHandleException(e = e)
    }

    newState.notifyCompletion(result)
    newState.clear()
}

/**
 * 尝试处理异常,分发前先做静默过滤
 */
private fun tryHandleException(e: Throwable): Boolean {
    return when (e) {
        is CancellationException -> {
            // 忽略正常的取消控制流,避免将其视为程序崩溃
            false
        }

        else -> {
            // 将常规未捕获异常交由子类处理
            handleJobException(e)
        }
    }
}

为什么要过滤掉 CancellationException

挂起函数取消时,会通过抛出取消异常来实现取消响应,它是正常的流程,类似于线程的中断异常。所以需要忽略,不能让它触发崩溃处理逻辑。而未捕获异常为什么要向上传递,这就涉及到了协程的作用域了,我将会在下一篇博客中详细讲解。

异常处理器的实战演示

最后我们来演示一下异常处理器是如何接受崩溃的:

kotlin 复制代码
suspend fun main() {
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        println("CoroutineExceptionHandler got ${throwable.message}, from ${coroutineContext[CoroutineName]}")
    }

    launch(context = exceptionHandler + CoroutineName("main")) {
        println("Started main coroutine")
        throw ArithmeticException("Dive by zero")
        println("Ended main coroutine")
    }.join()

    delay(time = 200L)
}

因为我们之前有注入默认的调度器,协程完成时会在守护线程中执行,此时主线程可能会提前退出,所以加一个延迟保证异常处理器的执行。

结果输出为:

Plaintext 复制代码
Started main coroutine
CoroutineExceptionHandler got Dive by zero, from main
相关推荐
百锦再5 小时前
Auto.js变成基础知识学习
开发语言·javascript·学习·sqlite·kotlin·android studio·数据库开发
帅次9 小时前
Compose 入门:@Composable、组合与重组
android·kotlin·gradle·android jetpack·compose·composable
Junerver10 小时前
Kotlin - 约定contract
kotlin
Junerver11 小时前
使用datetime更加优雅地在kotlin中处理时间
kotlin
装杯让你飞起来啊13 小时前
第 4 周 Unit 2:Jetpack Compose 状态、按钮、计数器与小费计算器
windows·microsoft·kotlin·安卓
Kapaseker18 小时前
MVVM 旧城改造,边界划分各有招
android·kotlin
装杯让你飞起来啊1 天前
第 2 周 Day 5-6:综合小游戏 —— 学生成绩管理系统
windows·microsoft·kotlin
装杯让你飞起来啊2 天前
Kotlin List / Array 与 for 循环
开发语言·kotlin·list
装杯让你飞起来啊2 天前
混合练习 —— 猜数字游戏
windows·游戏·kotlin