协程取消全攻略:掌握“协作式”资源回收的艺术

37 阅读2分钟

在并发编程中,强行停止一个线程是极度危险且已过时的做法(如 Thread.stop())。Kotlin 协程采取了完全不同的哲学:协作式取消(Cooperative Cancellation) 。这意味着取消不是一种命令,而是一次“请求”——协程需要主动检查自己是否被取消,并优雅地交出执行权。


一、 取消的触发:Scope 与 Job 的联动

在协程中,取消是沿着 Job 层级结构 传播的。

  1. 取消作用域 (Scope): 调用 scope.cancel() 会取消该作用域启动的所有子协程。
  2. 取消特定 Job: 调用 job.cancel() 只会取消该 Job 及其子项,不影响父级及兄弟级。

关键操作符对比

方法行为说明
job.cancel()发出取消请求,但不等待协程真正结束。
job.join()挂起当前协程,直到该任务运行结束。
job.cancelAndJoin()发出取消请求并挂起,直到该任务彻底清理完毕。推荐用于确保资源释放的场景。

二、 取消的核心:协作原则

划重点:如果协程正在执行繁重的 CPU 计算任务且没有检查取消状态,它将无法被取消。

如何让你的协程“可被取消”?

要让协程具备协作性,你有两种主流方案:

1. 周期性检查 isActive

这是最直接的办法。在 while 循环或耗时逻辑中检查协程的存活状态。

Kotlin

val job = scope.launch(Dispatchers.Default) {
    var i = 0
    while (i < 5 && isActive) { // 检查 isActive 标志位
        // 模拟耗时计算
        println("Hello $i")
        i++
    }
}

2. 使用 ensureActive()yield()

  • ensureActive() :如果 Job 已取消,它会立即抛出 CancellationException
  • yield() :不仅检查取消状态,还会让出 CPU 执行权,让其他协程有机会运行,是处理重度循环的推荐方式。

Kotlin

val job = scope.launch(Dispatchers.Default) {
    for (i in 1..1000) {
        ensureActive() // 如果已取消,直接抛出异常结束
        // 或者使用 yield()
        // yield() 
        executeComputation()
    }
}

三、 挂起函数:天生支持取消

绝大多数官方提供的挂起函数(如 withContextdelay 等)都是可取消的。它们在执行前都会检查当前协程的状态,并在取消时抛出 CancellationException

如果你在 try-catch 中捕获了 Exception,请务必注意:不要吞掉 CancellationException

Kotlin

// ❌ 错误示范:吞掉取消异常
try {
    work()
} catch (e: Exception) {
    // 捕获了所有异常,包括 CancellationException
    // 导致协程无法正常进入取消状态
    log("Error: $e")
}

// ✅ 正确示范:重新抛出
try {
    work()
} catch (e: CancellationException) {
    throw e // 必须重新抛出,让协程机制正常运行
} catch (e: Exception) {
    log("Real error: $e")
}

四、 清理收尾:finallyNonCancellable

当协程被取消时,它会进入 Cancelling 状态并执行 finally 块中的逻辑(例如关闭数据库连接)。

陷阱:在 finally 中调用挂起函数

当协程处于 Cancelling 状态时,普通的挂起函数会直接失效并继续抛出 CancellationException。如果你必须在清理时执行挂起操作(如:向服务器发送“取消成功”的通知),你需要使用特殊的上下文。

Kotlin

val job = scope.launch {
    try {
        doWork()
    } finally {
        // ❌ 这里直接调用挂起函数会失败
        // cleanupWork() 

        // ✅ 使用 NonCancellable 上下文
        withContext(NonCancellable) {
            println("正在执行最后的清理工作...")
            delay(1000) // 现在可以正常执行挂起操作了
            println("清理完毕")
        }
    }
}

五、 最佳实践总结

  1. 保持协作: 确保耗时的计算逻辑中包含 isActive 检查或调用 yield()
  2. 不滥用 NonCancellable 它非常强大但也危险,仅用于资源释放逻辑,不要在里面执行核心业务逻辑。
  3. 理解异常模型: CancellationException 不会导致应用崩溃,它是协程用来正常终止流程的信号。
  4. 优先使用封装: 尽量利用官方提供的 withTimeout 等自带取消机制的函数。