【问题标题】:kotlin coroutine withTimeout does not cancel when using withContext to get non-blocking codekotlin协程withTimeout在使用withContext获取非阻塞代码时不取消
【发布时间】:2019-10-11 18:55:32
【问题描述】:

我正在使用 withContext 将函数转换为不会阻塞调用线程的挂起函数。 为此,我使用了https://medium.com/@elizarov/blocking-threads-suspending-coroutines-d33e11bf4761 作为参考。

现在我想用超时调用这个函数。为此,我使用 withTimeout 来调用函数:

@Test
internal fun timeout() {
    runBlocking {
        logger.info("launching")
        try {
            withTimeout(1000) {
                execute()
            }
        } catch (e: TimeoutCancellationException) {
            logger.info("timed out", e)
        }
    }
}

private suspend fun execute() {
    withContext(Dispatchers.IO) {
        logger.info("sleeping")
        Thread.sleep(2000)
    }
}

所以我希望在 1000 毫秒后取消异步启动的协程并抛出 TimeoutCancellationException。
但是会发生什么是完整的 2000 毫秒通过,当协程完成时抛出异常:

14:46:29.231 [main @coroutine#1] 信息 b.t.c.c.CoroutineControllerTest - 发射
14:46:29.250 [DefaultDispatcher-worker-1 @coroutine#1] 信息 b.t.c.c.CoroutineControllerTest - 睡眠
14:46:31.261 [main@coroutine#1] 信息 b.t.c.c.CoroutineControllerTest - 超时 kotlinx.coroutines.TimeoutCancellationException:等待超时 1000 毫秒 kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:128) 在 kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:94) 在 kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.kt:307) 在 kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.kt:116) 在 kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:68) 在 java.lang.Thread.run(Thread.java:748)

我用错了吗?

或者这可能是预期的行为?在文档中,计数器也变为 2,这意味着在取消协程之前已经过去了 1500 毫秒: https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/cancellation-and-timeouts.md#timeout

【问题讨论】:

  • Thread.sleep 阻塞当前线程(与delay 函数不同)并且无法取消(仅在Thread 类文档的意义上被中断或停止)。
  • 只要打电话给Thread.currentThread().interrupt(),你就可以找到TimeoutCancellationException。这将导致runBlockingInterruptedException 退出(但实际上不会取消操作)。

标签: kotlin kotlin-coroutines


【解决方案1】:

重读取消文档后,似乎协程必须配合才能取消:

协程取消是合作的。协程代码必须 合作可以取消。

https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#cancellation-is-cooperative

我还发现线程设计上不会被中断:

取消协程不会中断线程。这是由 设计,因为不幸的是,许多 Java 库不正确 在中断的线程中操作。

https://discuss.kotlinlang.org/t/calling-blocking-code-in-coroutines/2368/6

这解释了为什么代码要等待睡眠完成。
这也意味着不可能在阻塞线程添加超时的协程上使用 withTimeout。
当使用返回期货的非阻塞库时,可以使用 withTimeout,如下所述:

为了与取消正确集成, CompletableFuture.await() 使用与所有未来相同的约定 组合器 do — 如果 await 调用,它会取消底层的未来 自己被取消了。

https://medium.com/@elizarov/futures-cancellation-and-coroutines-b5ce9c3ede3a

文档中示例的旁注: 通过在延迟/超时示例中添加日志语句,我发现只有 1300 毫秒通过,因此延迟与 withTimeout 完美配合。

08:02:24.736 [main @coroutine#1] 信息 b.t.c.c.CoroutineControllerTest - 我在睡觉 0 ...
08:02:25.242 [main @coroutine#1] INFO b.t.c.c.CoroutineControllerTest - 我在睡觉 1 ...
08:02:25.742 [main @coroutine#1] 信息 b.t.c.c.CoroutineControllerTest - 我是 睡觉 2 ...
08:02:26.041 [main @coroutine#1] 信息 b.t.c.c.CoroutineControllerTest - 取消

【讨论】:

    【解决方案2】:

    如果您启动子协程并等待其完成,您可以在超时后取得进展:

    fun timeout() {
        runBlocking {
            logger.info("launching")
            try {
                withTimeout(100) {
                    execute()
                }
            } catch (e: TimeoutCancellationException) {
                logger.info("timed out", e)
            }
        }
    }
    
    private suspend fun execute() =
        GlobalScope.launch(Dispatchers.IO) {
            logger.info("sleeping")
            Thread.sleep(2000)
        }.join()
    

    这样你就可以将阻塞的子协程与你join()它的调度程序解耦,所以suspend fun join()可以立即对取消做出反应。

    请注意,这与其说是一个完整的解决方案,不如说是一种变通方法,因为IO 调度程序中的一个线程仍将保持阻塞状态,直到sleep() 过期。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-02-12
      • 2021-05-17
      • 1970-01-01
      • 2020-10-08
      • 1970-01-01
      • 2020-12-13
      相关资源
      最近更新 更多