说明调度程序更改时 withContext 行为与未更改时 withContext 行为的差异



withContext的Kotlin文档称

此函数使用新上下文中的调度器,如果指定了新的调度器,则将块的执行转移到不同的线程中,并在完成时返回到原始调度器。请注意,withContext调用的结果以可取消的方式被调度到原始上下文中,并具有提示取消保证,这意味着如果在其调度器开始执行代码时,调用withContext的原始协同上下文被取消,则它将丢弃withContext的结果并抛出CancellationException。

当且仅当调度程序正在更改时,才会启用上述取消行为。例如,当使用withContext(不可取消({…}时,调度程序中没有任何更改,并且无论是在进入withContext内部的块时还是从中退出时,此调用都不会被取消。

我试图设计代码,说明当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之前会得到取消。

考虑到这一更正,我会说文档是错误的,特别是这句话:

当且仅当调度程序正在更改时,才会启用上述取消行为。

实际上,在上下文中使用NonCancellable作业时,除了之外,所有情况下都会启用该行为。文档的下一句话对给定的具体示例进行了正确的描述:

例如,当使用withContext(NonCancellable) { ... }时,调度程序没有变化,无论是在进入withContext内部的块时,还是从中退出时,此调用都不会被取消。

但是,对任何不切换调度器的上下文的泛化似乎是错误的。

再次更新

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

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

最新更新