Kotlin 协程源代码泛读:协程取消 引子

177 阅读3分钟

这篇文章可以认为是深入分析协程取消机制的引子,从实例出发探讨协程取消的机理,最后对源码先做一些概要性介绍

先看一些协程取消的例子

fun testContinuationCancellable() {
    runBlocking {
        val job = launch(Dispatchers.IO) {
            try {
                println("job start")
                while (true) {
                    // busy loop
                    println('.')
                }
                println("job end")
            } catch (e: Exception) {
                println("job exception: ${e.message}")
            }
        }
        delay(1000)
        job.cancel()
    }
}

job.cancel 尝试取消协程,但是 while 将一直 busy loop

如果在 busy loop 中加入 delay

fun testContinuationCancellable() {
    runBlocking {
        val job = launch(Dispatchers.IO) {
            try {
                println("job start")
                while (true) {
                    // busy loop
                    println('.')
                    delay(1000L)
                }
                println("job end")
            } catch (e: Exception) {
                println("job exception: ${e.message}")
            }
        }
        delay(3000)
        job.cancel()
    }
}

Job.cancel 可以正常取消协程,程序的输出如下: job start . . . job exception: StandaloneCoroutine was cancelled

这两个函数的区别在哪?
testContinuationCancellable 在 busy loop 中调用了 suspend fun(可挂起)函数:

协程可以在可挂起点(suspend point)被取消

我们再来看看调用 suspend 函数的情况

fun testCancelSuspend() {
    runBlocking {
        val job = launch {
            doSuspend1()
            doSuspend2()
            doSuspend3()
        }
        delay(200L)
        job.cancel()
    }
}

suspend fun doSuspend1() {
    println("doSuspend1 start")
    val start = System.currentTimeMillis()
    while (true) {
        val now = System.currentTimeMillis()
        if (abs(now - start) >= 1000) {
            break
        }
    }
    println("doSuspend1 end")
}

suspend fun doSuspend2() {
    println("doSuspend2 start")
    delay(1000L)
    println("doSuspend2 end")
}

suspend fun doSuspend3() {
    println("doSuspend3 start")
    delay(1000L)
    println("doSuspend3 end")
}

doSuspend1 只是被声明成了 suspend 函数,本身并没有挂起点所以它无法被取消
doSuspend2(doSuspend3)可以正常被取消,程序输出:

doSuspend1 start
doSuspend1 end
doSuspend2 start

回顾系列文章中对 suspend 函数的解读

suspend 函数本质就是一个状态机,通过 continuation resumeWith 启动和执行

下面这段代码通过自定义 ContinuationInterceptor,intercepte(包装\代理\欺上瞒下)continuation,查看 suspend fun/Continaution 执行过程

class MyContinuationInterceptor(
    val context: CoroutineContext
) : ContinuationInterceptor {
    override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
        return MyContinuation(continuation, context)
    }

    override val key: CoroutineContext.Key<*>
        get() = ContinuationInterceptor.Key
}

private class MyContinuation<T>(
    val continuation: Continuation<T>,
    override val context: CoroutineContext
): Continuation<T> {
    override fun resumeWith(result: Result<T>) {
        println("MyContinuation resume ${continuation} with $result")
        continuation.resumeWith(result)
    }
}

fun testContinuationResumeWith() {
    runBlocking(
        context = MyContinuationInterceptor(EmptyCoroutineContext)
    ) {
        doSuspend()
        doSuspend()
        doSuspend()
    }
}

可以清除看到 Continuation 每次 resume

MyContinuation resume Continuation at com.dreamworker.kotlin.coroutine.ContinuationKt$testContinuationResumeWith$1.invokeSuspend(Continuation.kt) with Success(kotlin.Unit)

MyContinuation resume Continuation at com.dreamworker.kotlin.coroutine.SuspendKt.doSuspend(Suspend.kt:26) with Success(kotlin.Unit)

doSuspend

MyContinuation resume Continuation at com.dreamworker.kotlin.coroutine.SuspendKt.doSuspend(Suspend.kt:26) with Success(kotlin.Unit)

doSuspend

MyContinuation resume Continuation at com.dreamworker.kotlin.coroutine.SuspendKt.doSuspend(Suspend.kt:26) with Success(kotlin.Unit)

doSuspend

综上,可以做个猜想:

Kotlin 协程取消的 实现是一个契约式的实现,在挂起点 resume 的时候检查协程是否被取消了,如果取消了就终止状态机执行

协程框架为可取消的 Continuation 单独定了一个接口CancellableContinuation 使用 suspendCancellableCoroutine 方法可以获取 CancellableContinuation,然后实施检测 isCancelled 状态

fun testSuspendCancellableContinuation() {
    runBlocking {
        val job = launch(Dispatchers.IO) {
            suspendCancellableCoroutine<Unit> { c ->
                try {
                    println("job start")
                    while (true) {
                        // busy loop
                        println('.')
                        if (c.isCancelled) {
                            break
                        }
                    }
                    println("job end")
                } catch (e: Exception) {
                    println("job exception: ${e.message}")
                }
            }
        }
        job.cancel()
    }
}

  分析 suspendCancellableCoutine 可以清晰的了解 CcancellableContinuation 是如何获知 Job 被 Cancel 的

public suspend inline fun <T> suspendCancellableCoroutine(  
    crossinline block: (CancellableContinuation<T>) -> Unit  
): T =  
    suspendCoroutineUninterceptedOrReturn { uCont ->

        // 使用 CancellableContinuationImpl 对 uCont 进行拦截(wrap)
        // uCont 从挂起点恢复的时候,CanncelableContinuationImpl 会检查 Job 是否被 Cancel 了  
        val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)  
        /*  
         * For non-atomic cancellation we setup parent-child relationship immediately  
         * in case when `block` blocks the current thread (e.g. Rx2 with trampoline scheduler), but  
         * properly supports cancellation.  
         */  
        cancellable.initCancellability()  
        block(cancellable)  
        cancellable.getResult()  
    }

initCancellability 就是为了和 Job 建立联系

public override fun initCancellability() {
    /*
    * Invariant: at the moment of invocation, `this` has not yet
    * leaked to user code and no one is able to invoke `resume` or `cancel`
    * on it yet. Also, this function is not invoked for reusable continuations.
    */
    val handle = installParentHandle()
        ?: return // fast path -- don't do anything without parent
    // now check our state _after_ registering, could have completed while we were registering,
    // but only if parent was cancelled. Parent could be in a "cancelling" state for a while,
    // so we are helping it and cleaning the node ourselves
    if (isCompleted) {
        // Can be invoked concurrently in 'parentCancelled', no problems here
        handle.dispose()
        _parentHandle.value = NonDisposableHandle
    }
}

installParentHandle 查询 CoroutineContext 中的 Job(协程),注册 cancel complete handle 在 parent(Job/Coroutine)被 Cancel 的时候得到通知并设置取消状态

internal fun parentCancelled(cause: Throwable) {  
    if (cancelLater(cause)) return  
    cancel(cause)  
    // Even if cancellation has failed, we should detach child to avoid potential leak  
    detachChildIfNonResuable()  
}

CancellableContinuation 和 Job.cancel 的实现很复杂,篇幅所限暂不展开,对原理有个全局的了解后剩下具体分析应该就是工作量和时间问题