【问题标题】:Illustrate difference in withContext behavior when dispatcher changes vs when it doesn't说明调度程序更改与未更改时 withContext 行为的差异
【发布时间】:2021-07-19 07:17:47
【问题描述】:

Kotlin docs for withContext

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

当且仅当调度程序被更改时,才会启用上述取消行为。例如,当使用 withContext(NonCancellable) { ... } 时,调度程序没有变化,并且在进入 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,无论调度器是否切换。但这与文档相矛盾。

我错过了什么?我误解了文档吗?

【问题讨论】:

  • “我试图设计代码来说明调度程序更改与未更改时的取消行为之间的区别” - 请向我们展示您到目前为止所拥有的内容。
  • @Dai 会的。很乱,所以我没有把它包括在内。但我会清理它并添加到帖子中。
  • 你错过了这部分It immediately checks for cancellation of the resulting context and throws CancellationException if it is not active。在这种情况下,withContext 也不会将JobCancellationExceptionNonCancellable 一起抛出,因为NonCancellable 始终处于活动状态

标签: kotlin kotlin-coroutines


【解决方案1】:

文档的重点是 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 中传递的作业。

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

【讨论】:

  • 谢谢@marko-topolnik。我希望你能做出回应,因为我从你对协程的回答中学到的东西比 Stack Overflow 上的任何人都多。我对您提供的代码进行了轻微修改 - 我将启动休眠 1 秒,以便主线程有时间在第一个 withContext 执行之前取消作业。我在第一个withContext 周围做了一个尝试。一个CancellationException 被抛出。但它是由第一个 withContext 抛出的。不是第二个。这不是表明取消不是由第二个withContext 切换调度程序引起的吗?
  • 即使withContext 没有切换调度程序,它是否没有显示在进入第一个withContext 时正在检查job 的取消?从文档来看,这是我只希望第二个withContext 出现的行为。要点here
  • 实际上,我只是尝试修改您的代码,但在 launch 中没有 sleep。运行它具有相同的结果 - CancellationException 由第一个 withContext 抛出,而不是第二个,与文档相矛盾。
  • 确实,文档似乎是错误的,他们描述的行为仅适用于您使用 NonCancellable 时,而不是一般情况下,只要您不更改调度程序。
猜你喜欢
  • 1970-01-01
  • 2023-03-06
  • 1970-01-01
  • 2016-03-20
  • 1970-01-01
  • 1970-01-01
  • 2014-09-17
  • 2013-07-06
  • 1970-01-01
相关资源
最近更新 更多