线程的强制取消和交互式取消
线程的取消详细可以看我的这篇博客:Java 线程通信基础:interrupt、wait 和 notifyAll 详解
Java 早期提供了一个 Thread.stop()
方法来停止线程。只要调用它,目标线程就会立即被无条件的终止,无论线程当前在执行什么任务。
这种野蛮的取消方式可能会导致文件损坏(线程正在写入文件),也可能会导致状态不一致(因为 stop()
会介入原子性操作)。
所以它早被废弃了,现在 Java 线程的正确取消方式是 Thread.interrupt()
。
interrupt()
的工作机制是:它不会强行终止线程,只是会通知线程取消,线程内部需要主动配合。
配合的方式有两种:
-
主动检查 :在循环或是耗时操作前,主动检查中断状态,如果为
true
,就执行必要的清理工作后,主动退出。 -
被动响应 :许多阻塞方法,能够自动响应中断。当处于等待状态的线程被通知取消时,线程会被唤醒并抛出
InterruptedException
。
这样,线程被要求取消时,就能够进行收尾,从而保证程序状态的一致性。
协程的取消
在协程中,发送取消请求调用的方法是 Job.cancel()
,和线程中的 Thread.interrupt()
是类似的。
kotlin
val scope = CoroutineScope(Dispatchers.Default)
val job = scope.launch {
while (true) {
println("Coroutine is running...")
delay(1000)
}
}
delay(2500)
job.cancel()
运行结果:
less
Coroutine is running...
Coroutine is running...
Coroutine is running...
协程被取消了,但是你并没有在上述代码中看到交互式的影子,为什么呢?
我们先来说说协程的主动检查 。在协程中,取消标志位是 isActive
属性(在 Job
类中),它表示协程是否活跃。如果标志位变为 false
,应该手动抛出 CancellationException
异常来取消协程,而非直接 return
。
CancellationException
是协程取消机制的核心,它是一种特殊的异常。当我们抛出它时,它会被用于正常地取消协程,而不会导致程序崩溃。
(为什么不直接 return
?因为 return
只会取消当前协程的代码块,但协程是结构化的,这样会导致其子协程无法被取消。)
kotlin
val scope = CoroutineScope(Dispatchers.Default)
val job = scope.launch {
// 可以通过CoroutineScope.isActive扩展属性来方便地检查
while (isActive) {
println("Coroutine is computing...")
Thread.sleep(500) // 模拟耗时且非挂起的计算
}
}
delay(1300)
job.cancel()
也可以通过
CoroutineScope.ensureActive()
扩展函数来检查,它会在协程不活跃时,自动抛出CancellationException
。
现在,我们来回答为什么最开始的代码中,协程被取消了。
关键在于协程的被动响应 ,几乎所有 kotlinx.coroutines
库中的挂起函数(如 delay
、withContext
)都是可取消的,它们在内部会自动响应取消请求(收到请求,会立即抛出 CancellationException
来中止执行)。
有个例外,底层的
suspendCoroutine
挂起函数并不能自动响应取消。如果要构建支持取消的自定义挂起逻辑,应该使用suspendCancellableCoroutine
。
所以,协程被取消是因为使用了 delay()
函数,这也让我们省去了不必要的手动检查。
清理工作
由于协程的取消是通过异常完成的,所以要在清理工作完成后将异常再次抛出:
kotlin
val job = scope.launch {
try {
println("Coroutine is running...")
delay(1000)
} catch (e: CancellationException) {
println("Cleaning up...")
// ...清理工作...
throw e
}
}
job.cancel()
如果清理工作无论如何都要执行(不管协程是正常结束还是被取消),最好使用 try-finally
:
kotlin
val job = scope.launch {
try {
println("Coroutine is running...")
delay(1000)
} finally {
// ...清理工作...
println("Finally cleaning up!")
}
}
job.cancel()
结构化取消
结构化取消(Structured Cancellation):当一个父协程被取消时,取消的请求也会传播给其所有子协程。
结构化取消在我的这篇博客中有讲到:Kotlin 协程的灵魂:结构化并发详解
kotlin
val scope = CoroutineScope(Dispatchers.Default)
val parentJob = scope.launch { // 父协程
launch { // 子协程 1
println("子协程 1 开始")
delay(3000)
println("子协程 1 结束")
}
launch { // 子协程 2
println("子协程 2 开始")
delay(3000)
println("子协程 2 结束")
}
println("父协程结束")
}
delay(1500)
parentJob.cancel()
上述代码的运行结果是:
less
子协程 1 开始
父协程结束
子协程 2 开始
可以看到,在取消父协程后,两个子协程也一并被取消了。
现在,有个问题:子协程能够拒绝取消吗?
答案是:基本不能 。因为子协程无法阻止自己的 isActive
状态变为 false
,也无法阻止取消通知向下传递。
协程内部在抛出
CancellationException
异常时,也会将自己的isActive
状态变为false
,并调用所有子协程的Job.cancel()
,这是用于协程主动自我取消的。
虽然可以通过捕获异常阻止 CancellationException
异常的向上传递,继续执行后续代码。不过,这么做是十分不推荐的,因为它破坏了结构化并发。
kotlin
val scope = CoroutineScope(Dispatchers.Default)
val parentJob = scope.launch {
launch { // 子协程
println("子协程开始执行")
try {
delay(3000)
} catch (_: CancellationException) {
// 吞掉异常,不让它传播
}
println("子协程执行中...")
Thread.sleep(1500)
println("子协程结束执行")
}
}
delay(100)
parentJob.cancel()
measureTime {
parentJob.join()
}.also {
println("父协程耗时:${it}")
}
运行结果:
less
子协程开始执行
子协程执行中...
子协程结束执行
父协程耗时:1.502403800s
可以看到,这个子协程拖住了父协程,让父协程无法及时完成。
不配合取消的 NonCancellable
有时,我们需要某个协程不响应取消。为此,我们可以使用 NonCancellable
。
kotlin
val scope = CoroutineScope(Dispatchers.Default)
val parentJob = scope.launch { // 父协程
launch(context = NonCancellable) { // 子协程
println("Child job started")
delay(2000)
println("Child job ended")
}
}
delay(1000)
parentJob.cancel()
运行结果:
less
Child job started
Child job ended
可以看到,即使取消了父协程,子协程还是执行完了所有代码。
这是怎么做到的呢?
其实,launch(NonCancellable)
启动的协程与外部协程并不是父子关系,所以在取消外部的协程时,并不会将取消请求传递给它。NonCancellable
本身是一个特殊的 Job
对象,它的作用就是切断这种关联。
NonCancellable
主要用于不希望被取消的任务,比如:
-
清理工作 :如果清理代码中包含挂起函数(如往数据库写入数据),它可能会因为协程已是取消状态而失败。所以,要将这类清理逻辑放在
withContext(NonCancellable)
代码块中。虽说
withContext
也是一个可取消的挂起函数,但finally
块是特殊的清理阶段,协程框架为了清理逻辑能可靠地执行,在这允许withContext
启动。kotlintry { // ... } finally { withContext(NonCancellable) { // 这个块内的挂起函数不会被取消 logToDatabase("Operation cancelled.") delay(1000L) } }
-
执行原子性且很难回滚的操作:比如复杂文件的写入,一旦开始,中途停下并撤销的逻辑太复杂,不如直接让它不可取消。
kotlinwithContext(Dispatchers.IO + NonCancellable) { writeFileAtomically(...) }
-
与当前协程业务无关的任务 :比如独立的日志上报,它的生命周期应该与当前业务协程解耦,不因随着当前协程的取消而中止。
kotlinlaunch(NonCancellable) { globalLog(...) }