在并发编程中,强行停止一个线程是极度危险且已过时的做法(如 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()
}
}
三、 挂起函数:天生支持取消
绝大多数官方提供的挂起函数(如 withContext、delay 等)都是可取消的。它们在执行前都会检查当前协程的状态,并在取消时抛出 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")
}
四、 清理收尾:finally 与 NonCancellable
当协程被取消时,它会进入 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等自带取消机制的函数。