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是在文档中还是在实现中。