使用withContext获取非阻塞代码时,kotlin协程withTimeout不会取消

时间:2019-05-25 12:59:20

标签: kotlin kotlin-coroutines

我正在使用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] INFO 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

2 个答案:

答案 0 :(得分:0)

重新阅读取消文件后,协程似乎必须合作才能取消:

  

协程取消是合作的。协程代码必须   合作才能取消。

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()使用与所有将来相同的约定   组合器会这样做—如果等待调用,它将取消潜在的未来   本身被取消了。

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

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

  

08:02:24.736 [main @ coroutine#1] INFO 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] INFO b.t.c.c. CoroutineControllerTest-我是   睡觉2 ...
  08:02:26.041 [main @ coroutine#1] INFO b.t.c.c. CoroutineControllerTest-已取消

答案 1 :(得分:0)

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

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()到期之前仍将保持阻塞状态。