сопрограмма kotlin withTimeout не отменяется при использовании withContext для получения неблокирующего кода - PullRequest
1 голос
/ 25 мая 2019

Я использую 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] INFO b.t.c.c.CoroutineControllerTest - sleep
14: 46: 31.261 [main @ coroutine # 1] INFO 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 голосов
/ 28 мая 2019

Вы можете достичь прогресса после тайм-аута, если запустите детскую сопрограмму и ждете ее завершения:

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().

0 голосов
/ 27 мая 2019

После перечитывания документации об отмене кажется, что сопрограммы должны сотрудничать, чтобы быть отменяемыми:

Отмена сопрограммы является кооперативной.Код сопрограммы должен взаимодействовать, чтобы его можно было отменить.

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 () использует то же соглашение, что и все будущие комбинаторы - он отменяет базовое будущее, если отменен сам вызов await.

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

Дополнительное примечание к примеру из документации: добавив операторы журнала в пример задержки / тайм-аута, я обнаружил, что проходит всего 1300 миллисекунд, поэтому задержка отлично работает с withTimeout.

08: 02: 24.736 [main @ coroutine # 1] ИНФОРМАЦИЯ btccCoroutineControllerTest - я сплю 0 ...
08: 02: 25.242 [main @ coroutine # 1] ИНФОРМАЦИЯ btccCoroutineControllerTest - я сплю 1 ...
08: 02: 25.742 [main @ coroutine # 1] ИНФОРМАЦИЯ btccCoroutineControllerTest - Я сплю 2 ...
08: 02: 26.041 [main @ coroutine # 1] INFO btccCoroutineControllerTest - отменено

...