位置:首页 > Kotlin > 协程取消机制详解:掌握协作式资源回收方法

协程取消机制详解:掌握协作式资源回收方法

时间:2026-08-20  |  作者:风起客  |  阅读:0

在并发编程领域,像 Thread.stop() 这种强行停止线程的做法,不仅危险重重,而且早已过时。

Kotlin协程则秉持着一种截然不同的理念:协作式取消(Cooperative Cancellation)

这意味着,取消并非是一种强制性的命令,而是一种“请求”。协程需要主动检查自身是否已被取消,然后以优雅的方式交出执行权。

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


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

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

  • 取消作用域 (Scope): 调用 scope.cancel() 会取消该作用域启动的所有子协程。
  • 取消特定 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("清理完毕")
        }
    }
}

五、最佳实践总结

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

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多