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

CoroutineScope取消时delay未抛出CancellationException的原因

为什么delay函数在协程取消后未抛出CancellationException?

delay函数的文档说明:“该挂起函数是可取消的。如果当前协程的Job在该挂起函数等待期间被取消或完成,此函数会立即以CancellationException恢复执行。”

但实际测试中未观察到该异常抛出,delay反而等待了完整的10秒才继续执行,这是为什么?

测试代码

import kotlinx.coroutines.*
import java.util.concurrent.CancellationException

class MyClass {
    private val scope = CoroutineScope(SupervisorJob() + CoroutineName("X coroutine"))

    init {
        scope.launch {
            delay(500)
            println("cancelling the scope")
            scope.cancel(CancellationException("cancelling the scope"))
        }
    }

    suspend fun callScopedFunction(
        func: suspend CoroutineScope.() -> Int
    ): Int {
        if (scope.isActive) {
            println("callScopedFunction scope is active")
        }
        return scope.func()
    }
}

fun main() {
    runBlocking {
        val result = MyClass().callScopedFunction {
            println("coroutineContext.job.isCancelled: ${coroutineContext.job.isCancelled}")
            delay(10000)
            println("coroutineContext.job.isCancelled: ${coroutineContext.job.isCancelled}")
            250
        }
        println(result)
    }
}

输出结果

callScopedFunction scope is active
coroutineContext.job.isCancelled: false
cancelling the scope
coroutineContext.job.isCancelled: true
250

原因分析

1. 混淆了两个不同的Job状态

你在func lambda中打印的coroutineContext.job.isCancelled,指向的是MyClass内部scope的Job状态,而非执行delay的协程的Job状态。

因为func是CoroutineScope.()的扩展函数,lambda里的coroutineContext等价于接收者scope的上下文属性。当你调用scope.cancel()后,scope的Job被取消,所以这个值会变成true,但这和执行delay的协程无关。

2. delay只检查当前执行协程的Job

delay函数的可取消性,是针对执行它的那个协程的Job而言的。在你的代码中,delay(10000)是在runBlocking启动的协程中执行的——这个协程的Job从未被取消,所以delay会正常等待10秒,不会抛出异常。

3. scope.cancel()的作用范围有限

scope.cancel()只会取消scope的Job,以及所有通过scope.launch/scope.async等构建器启动的子协程。但你的func并没有在scope的协程上下文中执行:scope.func()只是把scope作为接收者调用扩展函数,并没有将执行逻辑切换到scope的协程上下文里,因此func的执行不受scope.cancel()的影响。


修复方案

要让delay在scope取消时抛出异常,需要将func的执行切换到scope的协程上下文中,比如使用async或withContext:

// 修改callScopedFunction方法
suspend fun callScopedFunction(
    func: suspend CoroutineScope.() -> Int
): Int {
    if (scope.isActive) {
        println("callScopedFunction scope is active")
    }
    // 用async将func切换到scope上下文执行,再await获取结果
    return scope.async { func() }.await()
}

修改后,当scope.cancel()被调用时,func所在的协程会被取消,delay会立即抛出CancellationException,符合文档描述的行为。


内容的提问来源于stack exchange,提问作者Alexander

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 10:47:05