Kotlin协程中try-finally/Closeable.use/synchronized的取消与关闭问题
Kotlin协程取消在常见结构中的行为疑问
我非常喜欢Kotlin协程,但实在无法理解协程取消在以下结构中的行为:
coroutineScope { - try-(catch-)finally { } - (Closeable { -> ... }).use { } - synchronized(gate) { } }
try-finally
示例代码
@Test fun `finally gets partially called`() { var notReachedEndOfTry = true var reachedBeginningOfFinally = false var notReachedEndOfFinally = true var reachedOuterFinally = false val awaiter = CompletableDeferred<Unit>() runBlocking { withContext(Dispatchers.Default) { val job = launch { try { try { awaiter.complete(Unit) // During delay the delay gets cancelled delay(1.seconds) // Not called: notReachedEndOfTry = false } finally { // Called! Why?!?!?!? reachedBeginningOfFinally = true // Suspends again and never returns delay(1.seconds) // Not called: notReachedEndOfFinally = false } } finally { // This gets called!??!!?? reachedOuterFinally = true } } launch { awaiter.await() delay(500.milliseconds) job.cancel() } } } assert(notReachedEndOfTry) assert(reachedBeginningOfFinally) assert(notReachedEndOfFinally) assert(reachedOuterFinally) }
疑问
我原本未预期外层finally会执行,为何内层finally在delay()后被“取消”,外层finally仍会执行?这是编译器的转换技巧吗?是否总能保证finally被执行?
解答
这是Kotlin协程取消机制的设计特性:
- 协程取消是协作式的,只有执行到可挂起函数时才会响应取消。调用
job.cancel()仅标记协程的取消状态,不会立即中断正在执行的代码。 - 内层finally块启动时,协程已被标记取消,但非挂起逻辑(
reachedBeginningOfFinally = true)会正常执行;直到调用delay(1.seconds)这个可挂起函数时,协程检测到取消状态,抛出CancellationException并退出内层finally块。 - 外层finally块执行,是因为内层finally抛出的
CancellationException会被协程异常处理机制捕获,进而触发外层try块的finally逻辑。这是协程对异常传播和finally语义的保障,并非编译器的转换技巧。 - 并非总能保证finally执行:如果finally块中没有可挂起函数,且协程所在线程被强制终止(如
Thread.interrupt()),或是协程进入finally前遭遇致命错误,finally可能无法执行。但常规协作取消场景下,finally块中未被挂起函数中断的代码会执行,外层finally也会因内层异常触发而执行。
Closeable.use {}
示例代码
@Test fun `resource will not be closed`() { var reachedBeginningOfUse = false var notClosed = true val closeable = Closeable { notClosed = false } val awaiter = CompletableDeferred<Unit>() runBlocking { withContext(Dispatchers.Default) { val job = launch { closeable.use { reachedBeginningOfUse = true awaiter.complete(Unit) delay(1.seconds) } } launch { awaiter.await() delay(500.milliseconds) job.cancel() } assert(reachedBeginningOfUse) assert(notClosed) } } }
疑问与修正
此处资源不会被关闭! 编辑:资源会被关闭,断言不应放在runBlocking块内。🤦♂️
我知道可以使用suspendCancellableCoroutine { invokeOnCancellation { ... } },但这是正确方案吗?
解答
Closeable.use本身通过try-finally语义保证资源关闭,只要use块已进入,即使协程被取消,资源关闭的finally逻辑也会执行。之前的断言错误是因为断言放在runBlocking内部时,协程取消逻辑尚未完成,资源还未关闭。- 对于普通
Closeable资源,无需额外使用suspendCancellableCoroutine,use函数已足够处理协程取消场景。仅当需要自定义挂起函数的取消逻辑时,才需要用到suspendCancellableCoroutine和invokeOnCancellation。
synchronized(gate) {}
对于synchronized(gate) {},我已经了解可结合条件的ReentrantLock、Mutex和Semaphore替代方案。
内容的提问来源于stack exchange,提问作者Teneko
相关产品推荐
相关产品推荐

