协程取消源码解读

411 阅读4分钟

回顾携程

先梳理一下协程的执行流程,这一点真是每次不看每次忘,需要时时拿出来温习。

1.launch开启一个协程,构建coroutine,调用coroutine.start()

2.构建coroutineStart,调用invoke函数

3.调用 block.startCoroutine(receiver, completion)

4.coroutine最核心代码:

    createCoroutineUnintercepted(receiver, completion).intercepted().resume(Unit)

上面这行代码实际是构建了continuation,然后调用 intercepted(),最后调用resume方法。

5.找到continuation的实现类:BaseContinuationImpl,查看resume()方法。

6.开启了一个while循环,然后调用invokeSuspend() 方法, 这个invokeSuspend()即是我们写的kotlin代码生成的方法。

** 协程本质是利用语法糖帮我们实现异步回调,用结构化并发的思想去写代码。需要注意的是每次遇到挂起函数,后面的方法调用都可以理解为封装到挂起函数的回调里面。 **

猜测

根据上面的流程,我们知道协程的核心其实是 continuation.resume()方法。 如果协程想要取消,猜测是continuation.resume(error)这种情况,取消挂起函数。

协程的取消流程:

1.入口代码

public fun CoroutineContext.cancel(cause: CancellationException? = null) {
    this[Job]?.cancel(cause)
}

2.JobSupport.cancel()

public override fun cancel(cause: CancellationException?) {
    cancelInternal(cause ?: defaultCancellationException())
}

3.JobSupport.cancelImpl()方法,发现kotlin源码特别喜欢用impl关键字,算是一种命名规范吧

internal fun cancelImpl(cause: Any?): Boolean {
    var finalState: Any? = COMPLETING_ALREADY
    if (onCancelComplete) {
        // make sure it is completing, if cancelMakeCompleting returns state it means it had make it
        // completing and had recorded exception
        finalState = cancelMakeCompleting(cause)
        if (finalState === COMPLETING_WAITING_CHILDREN) return true
    }
    if (finalState === COMPLETING_ALREADY) {
        finalState = makeCancelling(cause)  --------> 核心是这个
    }
    return when {
        finalState === COMPLETING_ALREADY -> true
        finalState === COMPLETING_WAITING_CHILDREN -> true
        finalState === TOO_LATE_TO_CANCEL -> false
        else -> {
            afterCompletion(finalState)
            true
        }
    }
}

4. JobSupport.makeCancelling()方法

    private fun makeCancelling(cause: Any?): Any? {
    var causeExceptionCache: Throwable? = null // lazily init result of createCauseException(cause)
    loopOnState { state ->
        when (state) {
            is Finishing -> { // already finishing -- collect exceptions
                notifyRootCause?.let { notifyCancelling(state.list, it) } -----> 通知关闭
                return COMPLETING_ALREADY 
            }
            is Incomplete -> {
         
            }
            else -> return TOO_LATE_TO_CANCEL // already complete
        }
    }
}

5. JobSupport.notifyCancelling()方法,最核心代码,第三行是决定是否取消父协程,关于supervisorJob有一些知识点

    private fun notifyCancelling(list: NodeList, cause: Throwable) {
    // first cancel our own children
    onCancelling(cause)
    notifyHandlers<JobCancellingNode>(list, cause)
    // then cancel parent
    cancelParent(cause) // tentative cancellation -- does not matter if there is no parent
}

6. JobSupport.notifyHandlers() 通知节点

        private inline fun <reified T: JobNode> notifyHandlers(list: NodeList, cause: Throwable?) {
    var exception: Throwable? = null
    list.forEach<T> { node ->
        try {
            node.invoke(cause)
        } catch (ex: Throwable) {
            exception?.apply { addSuppressedThrowable(ex) } ?: run {
                exception =  CompletionHandlerException("Exception in completion handler $node for $this", ex)
            }
        }
    }
    exception?.let { handleOnCompletionException(it) }
}

7. ChildContinuation.invoke()

        internal class ChildContinuation(
    @JvmField val child: CancellableContinuationImpl<*>
        ) : JobCancellingNode() {
            override fun invoke(cause: Throwable?) {
                child.parentCancelled(child.getContinuationCancellationCause(job))
            }
        }

8. CancellableContinuationImpl.parentCancelled()

    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()
    }

9. CancellableContinuationImpl.cancel()

        public override fun cancel(cause: Throwable?): Boolean {
        _state.loop { state ->
            if (state !is NotCompleted) return false // false if already complete or cancelling
            // Active -- update to final state
            val update = CancelledContinuation(this, cause, handled = state is CancelHandler)
            if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure
            // Invoke cancel handler if it was present
            (state as? CancelHandler)?.let { callCancelHandler(it, cause) }
            // Complete state update
            detachChildIfNonResuable()
            dispatchResume(resumeMode) // no need for additional cancellation checks
            return true
        }
    }

10. DispatchedTask

        internal fun <T> DispatchedTask<T>.dispatch(mode: Int) {
            assert { mode != MODE_UNINITIALIZED } // invalid mode value for this method
            val delegate = this.delegate
            val undispatched = mode == MODE_UNDISPATCHED
            if (!undispatched && delegate is DispatchedContinuation<*> && mode.isCancellableMode == resumeMode.isCancellableMode) {
                // dispatch directly using this instance's Runnable implementation
                val dispatcher = delegate.dispatcher
                val context = delegate.context
                if (dispatcher.isDispatchNeeded(context)) {
                    dispatcher.dispatch(context, this)
                } else {
                    resumeUnconfined()
                }
            } else {
                // delegate is coming from 3rd-party interceptor implementation (and does not support cancellation)
                // or undispatched mode was requested
                resume(delegate, undispatched)
            }
        }

11. DispatchedTask.run()方法

        public final override fun run() {
        assert { resumeMode != MODE_UNINITIALIZED } // should have been set before dispatching
        val taskContext = this.taskContext
        var fatalException: Throwable? = null
        try {
            val delegate = delegate as DispatchedContinuation<T>
            val continuation = delegate.continuation
            withContinuationContext(continuation, delegate.countOrElement) {
                val context = continuation.context
                val state = takeState() // NOTE: Must take state in any case, even if cancelled
                val exception = getExceptionalResult(state)
                /*
                 * Check whether continuation was originally resumed with an exception.
                 * If so, it dominates cancellation, otherwise the original exception
                 * will be silently lost.
                 */
                val job = if (exception == null && resumeMode.isCancellableMode) context[Job] else null
                if (job != null && !job.isActive) {
                    val cause = job.getCancellationException()
                    cancelCompletedResult(state, cause)
                    continuation.resumeWithStackTrace(cause)
                } else {
                    if (exception != null) {
                        continuation.resumeWithException(exception)
                    } else {
                        continuation.resume(getSuccessfulResult(state))
                    }
                }
            }
        } catch (e: Throwable) {
            // This instead of runCatching to have nicer stacktrace and debug experience
            fatalException = e
        } finally {
            val result = runCatching { taskContext.afterTask() }
            handleFatalException(fatalException, result.exceptionOrNull())
        }
    }

12. Continuation扩展方法

        @InlineOnly
        public inline fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit =
            resumeWith(Result.failure(exception))

13. CancellableContinuationImpl.resumeImpl()

        private fun resumeImpl(
        proposedUpdate: Any?,
        resumeMode: Int,
        onCancellation: ((cause: Throwable) -> Unit)? = null
        ) {
            _state.loop { state ->
                when (state) {
                    is NotCompleted -> {
                        val update = resumedState(state, proposedUpdate, resumeMode, onCancellation, idempotent = null)
                        if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure
                        detachChildIfNonResuable()
                        dispatchResume(resumeMode) // dispatch resume, but it might get cancelled in process
                        return // done
                    }
                    is CancelledContinuation -> {
                        /*
                         * If continuation was cancelled, then resume attempt must be ignored,
                         * because cancellation is asynchronous and may race with resume.
                         * Racy exceptions will be lost, too.
                         */
                        if (state.makeResumed()) { // check if trying to resume one (otherwise error)
                            // call onCancellation
                            onCancellation?.let { callOnCancellation(it, state.cause) }
                            return // done
                        }
                    }
                }
                alreadyResumedError(proposedUpdate) // otherwise, an error (second resume attempt)
            }
        }

历时12步,终于知道协程是如何取消的了,不得不说,协程取消实在太麻烦了。

总结

我们可以了解到的知识点:

1.协程取消是针对挂起函数后面的代码进行取消,如delay(),withContext(),如果没有挂起函数,协程的任务会继续进行执行。

2.对于没有挂起函数的情况下,可以使用 isActive,或者 ensureActive() 直接在协程里面跑取消异常,只能抛出这个异常,协程结束。

3.协程内部处理了 CancellationException,可以理解为我们手动调用job.cacel() 其实也是在内部抛出了这个异常。 如果想处理这个异常,可以使用 withContext(NonCancellable),或者 withContext(timeOut)

4.我们构建coroutineContext,一般使用 SupervisorJob,直接忽略子协程的取消。

private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
	override fun childCancelled(cause: Throwable): Boolean = false
}

问题

子协程cancel,是否同步取消父协程?

public open fun childCancelled(cause: Throwable): Boolean {
    if (cause is CancellationException) return true
    return cancelImpl(cause) && handlesException
}

需要区分 SupervisorJob和普通Job类型

前者子协程取消跟自己无关,

后者需要区分取消是怎么造成的,如果是子协程调用cancel方法,和自己无关; 如果子协程异常了,自己也会取消,不过已经异常的话,直接程序崩溃了都。