说明调度程序更改时与不更改时 withContext 行为的区别

Illustrate difference in withContext behavior when dispatcher changes vs when it doesn't

Kotlin docs withContext

This function uses dispatcher from the new context, shifting execution of the block into the different thread if a new dispatcher is specified, and back to the original dispatcher when it completes. Note that the result of withContext invocation is dispatched into the original context in a cancellable way with a prompt cancellation guarantee, which means that if the original coroutineContext, in which withContext was invoked, is cancelled by the time its dispatcher starts to execute the code, it discards the result of withContext and throws CancellationException.

The cancellation behavior described above is enabled if and only if the dispatcher is being changed. For example, when using withContext(NonCancellable) { ... } there is no change in dispatcher and this call will not be cancelled neither on entry to the block inside withContext nor on exit from it.

我尝试设计代码来说明调度程序正在更改时取消行为与 withContext 未更改时取消行为之间的区别。但我无法复制文档中描述的预期差异。

无论是否切换调度程序,我都会看到相同的取消行为。

不使用 withContext 切换调度程序:

val scope: CoroutineScope = object : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + Job()
}
scope.run {
    val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
    val job1 = launch {
        println(1)
        try {
            withContext(NonCancellable) {
                println(2)
                delay(50)
            }
            println(3)
            withContext(CoroutineName("Foo")) { // <-- Not switching dispatcher
                println(4)
                withContext(NonCancellable) {
                    delay(100)
                }
                println(5)
            }
        } catch (e: Exception) {
            println(e)
        }
        println(6)
    }
    val job2 = launch {
        println(7)
        delay(10)
        job1.cancel()
    }
    println(8)
    joinAll(job1, job2)
    println(9)
}

输出:

1
8
7
2
3
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#41":StandaloneCoroutine{Cancelling}@6c57dbdd
6
9

切换调度器 withContext:

val scope: CoroutineScope = object : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + Job()
}
scope.run {
    val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
    val job1 = launch {
        println(1)
        try {
            withContext(NonCancellable) {
                println(2)
                delay(50)
            }
            println(3)
            withContext(dispatcher) { // <--- Switching dispatcher
                println(4)
                withContext(NonCancellable) {
                    delay(100)
                }
                println(5)
            }
        } catch (e: Exception) {
            println(e)
        }
        println(6)
    }
    val job2 = launch {
        println(7)
        delay(10)
        job1.cancel()
    }
    println(8)
    joinAll(job1, job2)
    println(9)
}

输出:

1
8
7
2
3
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#41":StandaloneCoroutine{Cancelling}@1f381518
6
9

我认为上面的比较表明,withContext在进入withContext块时会抛出CancellationException,无论调度器是否切换。但这与文档相矛盾。

我错过了什么?我是不是误解了文档?

文档的要点是 withContext 不会在开始和结束时引入自己的挂起点,除非您切换调度程序。所有其他可暂停功能继续照常工作,可以取消。

那么,这是您正在寻找的测试。有两个相同的不可暂停代码块,第一个在不切换调度程序的 withContext 中,第二个切换到 IO。第一个块总是运行完成,然后您会立即看到作业被取消。

import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import java.lang.Thread.currentThread
import java.lang.Thread.sleep
import java.util.concurrent.ThreadLocalRandom
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.NANOSECONDS
import kotlin.concurrent.thread
import kotlin.system.measureNanoTime
import kotlin.system.measureTimeMillis

fun main() {
    val job = GlobalScope.launch {
        withContext(CoroutineName("foo")) {
            val rnd = ThreadLocalRandom.current()
            val sum = (1..100_000_000).sumOf { rnd.nextInt() }
            println("Same dispatcher sum = $sum")
        }
        withContext(IO) {
            val rnd = ThreadLocalRandom.current()
            val sum = (1..100_000_000).sumOf { rnd.nextInt() }
            println("Changed dispatcher sum = $sum")
        }
    }
    job.invokeOnCompletion { cause -> println("job completed with $cause") }
    job.cancel()
    currentThread().join()
}

输出示例:

Same dispatcher sum = 1660606514
job completed with kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelled}@40f092d6

更新

正如 OP 所指出的,我的程序有一场比赛,因此第一个 withContext 在开始时错过了取消信号。如果我们将 sleep() 添加到 GlobalScope.launch 块的开头,我们会在进入第一个 withContext.

之前取消

鉴于此更正,我会说文档是错误的,特别是这句话:

The cancellation behavior described above is enabled if and only if the dispatcher is being changed.

实际上,似乎在所有情况下都启用了该行为 除了 当您在上下文中使用 NonCancellable 作业时。文档的下一句对给出的具体示例给出了正确的描述:

For example, when using withContext(NonCancellable) { ... } there is no change in dispatcher and this call will not be cancelled neither on entry to the block inside withContext nor on exit from it.

但是,对不切换调度程序的任何上下文的概括似乎是错误的。

再次更新...

我也尝试使用 withContext(Job()),将当前作业(已取消)的继承分解为 withContext 块。在这种情况下,也没有取消。所以行为似乎很复杂, coroutineScope.isActive 标志没有被注意到,但是 job.isActive 是,其中 job 指的是在 withContext.[=29 中传递的作业=]

由于实际行为似乎是自相矛盾的,因此很难说错误是在文档中还是在实现中。