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。这将导致runBlockingInterruptedException 退出(但实际上不会取消操作)。 【参考方案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获取非阻塞代码时不取消的主要内容,如果未能解决你的问题,请参考以下文章

对比Java学Kotlin协程-创建和取消

对比Java学Kotlin协程-创建和取消

Kotlin 协程协程底层实现 ① ( Kotlin 协程分层架构 | 基础设施层 | 业务框架层 | 使用 Kotlin 协程基础设施层标准库 Api 实现协程 )

Kotlin 协程协程底层实现 ① ( Kotlin 协程分层架构 | 基础设施层 | 业务框架层 | 使用 Kotlin 协程基础设施层标准库 Api 实现协程 )

Android 协程 超时任务

谈谈我对 Kotlin 中协程的理解