You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 14:53:19