这篇文章可以认为是深入分析协程取消机制的引子,从实例出发探讨协程取消的机理,最后对源码先做一些概要性介绍
先看一些协程取消的例子
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 的实现很复杂,篇幅所限暂不展开,对原理有个全局的了解后剩下具体分析应该就是工作量和时间问题