协程的取消机制
取消协程需要协程内部配合,这点和线程一样,本质上也是协作式的取消,就是将状态设置为取消,协程内部根据状态的变化来响应。
完善 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