kotlin协程withTimeout在使用withContext获取非阻塞代码时不取消
Posted
技术标签:
【中文标题】kotlin协程withTimeout在使用withContext获取非阻塞代码时不取消【英文标题】:kotlin coroutine withTimeout does not cancel when using withContext to get non-blocking code 【发布时间】: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
。这将导致runBlocking
以InterruptedException
退出(但实际上不会取消操作)。
【参考方案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()
过期。
【讨论】:
以上是关于kotlin协程withTimeout在使用withContext获取非阻塞代码时不取消的主要内容,如果未能解决你的问题,请参考以下文章
Kotlin 协程协程底层实现 ① ( Kotlin 协程分层架构 | 基础设施层 | 业务框架层 | 使用 Kotlin 协程基础设施层标准库 Api 实现协程 )
Kotlin 协程协程底层实现 ① ( Kotlin 协程分层架构 | 基础设施层 | 业务框架层 | 使用 Kotlin 协程基础设施层标准库 Api 实现协程 )