Kotlin协程中是否需要重新抛出CancellationException?
Kotlin协程中CancellationException的两个疑问
我正在学习《Kotlin Coroutine Deep Dive》深入理解协程,书中提到:
CancellationException 可以用try-catch捕获,但建议重新抛出它。
书中给出的示例代码如下:
import kotlinx.coroutines.* suspend fun main(): Unit = coroutineScope { val job = Job() launch(job) { try { repeat(1_000) { i -> delay(200) println("Printing $i") } } catch (e: CancellationException) { println(e) throw e } } delay(1100) job.cancelAndJoin() println("Cancelled successfully") delay(1000) }
我测试注释掉throw e后代码仍正常执行,因此疑惑不重新抛出CancellationException会有什么影响。
另外,我根据Kotlin文档理解该异常用于结构化并发,不会取消父协程,而是通知父协程取消其所有其他子协程,但如下测试代码显示,抛出该异常后其他子协程并未被取消,想了解我的理解是否有误及其中原因:
import kotlinx.coroutines.* fun main() = runBlocking { val parentJob = launch { launch { println("Starting child 1") delay(1000) throw CancellationException("Close other child under this parent") } launch { println("Starting child 2") delay(5000) println("This should not be printed, but getting printed. Don't know why?") } delay(2000) println("Parent coroutine completed") } parentJob.join() }
一、不重新抛出CancellationException的影响
- 协程状态异常:捕获
CancellationException却不重新抛出时,协程的内部状态会被标记为「完成」而非「取消」。表面代码能正常执行,但协程的Job对象会认为自己是正常结束,而非被主动取消。 - 结构化并发连锁逻辑失效:如果该协程是父协程的子协程,父协程无法感知到它被取消的事实。比如在
coroutineScope或supervisorScope中,父协程依赖子协程状态管理作用域生命周期,不抛出异常会导致父协程无法正确处理取消流程。 - 资源泄漏风险:协程被取消时,原本应通过
finally、use等机制释放的资源,可能因协程状态异常无法正确触发。比如持有网络连接、文件句柄等资源时,不传递取消信号可能导致资源未及时释放。
二、关于抛出CancellationException后子协程未取消的理解误区
你的理解存在偏差,CancellationException的默认行为并非「通知父协程取消其他子协程」,具体逻辑如下:
- 普通CancellationException的作用:子协程抛出未捕获的
CancellationException时,只会取消自身,不会影响父协程和其他子协程——除非这个异常是父协程发起的取消信号(比如调用job.cancel())。 - 结构化并发的异常传递规则:只有当子协程抛出非CancellationException的异常时,父协程才会被取消,进而连锁取消所有其他子协程。
CancellationException是协作取消的信号,默认不会触发父协程的取消流程。 - 测试代码的问题分析:你的代码中,子协程1主动抛出
CancellationException后,仅会取消自身;父协程和子协程2不受影响,因为这个异常不是父协程发起的取消指令。父协程会正常执行完delay(2000),子协程2的delay(5000)也会完成,因此会打印出预期外的内容。
如果要实现「子协程抛出异常后,父协程取消其他子协程」的效果,可以:
- 抛出非
CancellationException的自定义异常,触发父协程取消连锁; - 手动获取父协程的
Job,调用parentJob.cancel()主动取消父协程及所有子协程。
示例修改代码:
import kotlinx.coroutines.* fun main() = runBlocking { val parentJob = launch { launch { println("Starting child 1") delay(1000) // 手动取消父协程,进而取消所有子协程 coroutineContext.job.cancel() } launch { println("Starting child 2") delay(5000) println("This won't be printed now") } delay(2000) println("Parent coroutine won't complete either") } parentJob.join() }
内容的提问来源于stack exchange,提问作者Vencat
相关产品推荐
相关产品推荐

