Исключение, генерируемое deferred.await () в runBlocking, обработанном как необработанное, даже после перехвата - PullRequest
0 голосов
/ 09 ноября 2018

Этот код:

fun main() {
    runBlocking {
        try {
            val deferred = async { throw Exception() }
            deferred.await()
        } catch (e: Exception) {
            println("Caught $e")
        }
    }
    println("Completed")
}

приводит к выводу:

Caught java.lang.Exception
Exception in thread "main" java.lang.Exception
    at org.mtopol.TestKt$main$1$deferred$1.invokeSuspend(test.kt:11)
    ...

Такое поведение не имеет смысла для меня. Исключение было перехвачено и обработано, и все же оно переходит на верхний уровень как необработанное исключение.

Документируется и ожидается ли это поведение? Это противоречит всем моим представлениям о том, как должна работать обработка исключений.

Я адаптировал этот вопрос из ветки на форуме Kotlin .


Документы Kotlin предлагают использовать supervisorScope, если мы не хотим отменять все сопрограммы в случае сбоя. Так что я могу написать

fun main() {
    runBlocking {
        supervisorScope {
            try {
                launch {
                    delay(1000)
                    println("Done after delay")
                }
                val job = launch {
                    throw Exception()
                }
                job.join()
            } catch (e: Exception) {
                println("Caught $e")
            }
        }
    }
    println("Completed")
}

Вывод теперь

Exception in thread "main" java.lang.Exception
    at org.mtopol.TestKt$main$2$1$job$1.invokeSuspend(test.kt:16)
    ...
    at org.mtopol.TestKt.main(test.kt:8)
    ...

Done after delay
Completed

Опять же, это не то поведение, которое я хочу. Здесь launch ed сопрограмма завершилась неудачей с необработанным исключением, аннулировав работу других сопрограмм, но они продолжаются непрерывно.

Поведение, которое я нахожу разумным, заключается в распространении отмены, когда сопрограмма дает сбой непредвиденным (то есть необработанным) способом. Перехват исключения из await означает, что не было глобальной ошибки, только локализованное исключение, которое обрабатывается как часть бизнес-логики.

Ответы [ 4 ]

0 голосов
/ 10 февраля 2019

Хотя все ответы на месте, но позвольте мне пролить немного света на это, что может помочь другим пользователям. Документально здесь ( Официальный документ ), что: -

Если сопрограмма встречает исключение, отличное от CancellationException, он отменяет своего родителя с этим исключением. Такое поведение не может быть переопределяется и используется для обеспечения стабильных иерархий сопрограмм для структурированный параллелизм , не зависящий от CoroutineExceptionHandler реализация. Исходное исключение обрабатывается родителем (в GlobalScope), когда завершаются все его дочерние элементы.

Нет смысла устанавливать обработчик исключений для сопрограммы который запускается в объеме основного runBlocking , так как основной сопрограмма будет всегда отменяться, когда ее ребенок завершает за исключением несмотря на установленный обработчик.

Надеюсь, это поможет.

0 голосов
/ 09 ноября 2018

Нормальный CoroutineScope (который создается runBlocking) немедленно отменяет все дочерние сопрограммы, когда одна из них выдает исключение. Это поведение задокументировано здесь: https://kotlinlang.org/docs/reference/coroutines/exception-handling.html#cancellation-and-exceptions

Вы можете использовать supervisorScope, чтобы получить желаемое поведение. Если дочерняя сопрограмма выходит из строя внутри области супервизора, она не отменяет немедленно других детей. Дети будут отменены, только если исключение не обработано.

Подробнее см. Здесь: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html

fun main() {
    runBlocking {
        supervisorScope {
            try {
                val deferred = async { throw Exception() }
                deferred.await()
            } catch (e: Exception) {
                println("Caught $e")
            }
        }
    }
    println("Completed")
}
0 голосов
/ 09 ноября 2018

Изучив причины, по которым Котлин ввел такое поведение, я обнаружил, что, если исключения не будут распространяться таким образом, будет сложно написать корректный код, который будет своевременно отменен. Например:

runBlocking {
    val deferredA = async {
        Thread.sleep(10_000)
        println("Done after delay")
        1
    }
    val deferredB = async<Int> { throw Exception() }
    println(deferredA.await() + deferredB.await())
}

Поскольку a - это первый результат, которого мы ожидаем, этот код будет работать в течение 10 секунд, а затем приведет к ошибке и никакой полезной работе не будет достигнуто. В большинстве случаев мы хотели бы отменить все, как только один компонент выходит из строя. Мы могли бы сделать это так:

val (a, b) = awaitAll(deferredA, deferredB)
println(a + b)

Этот код менее элегантен: мы вынуждены ждать всех результатов в одном месте и теряем безопасность типов, потому что awaitAll возвращает список общего супертипа всех аргументов. Если у нас есть

suspend fun suspendFun(): Int {
    delay(10_000)
    return 2
}

и мы хотим написать

val c = suspendFun()
val (a, b) = awaitAll(deferredA, deferredB)
println(a + b + c)

Мы лишены возможности спастись до завершения suspendFun. Мы могли бы работать так:

val deferredC = async { suspendFun() }
val (a, b, c) = awaitAll(deferredA, deferredB, deferredC)
println(a + b + c)

но это хрупко, потому что вы должны остерегаться, чтобы убедиться, что вы делаете это для каждого приостановленного звонка. Это также противоречит доктрине Котлина о «последовательном по умолчанию»

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

0 голосов
/ 09 ноября 2018

Эту проблему можно решить, слегка изменив код, чтобы значение deferred выполнялось явно, используя ту же CoroutineContext, что и область действия runBlocking, например,

runBlocking {
    try {
        val deferred = withContext(this.coroutineContext) {
            async {
                throw Exception()
            }
        }
        deferred.await()
    } catch (e: Exception) {
        println("Caught $e")
    }
}
println("Completed")

ОБНОВЛЕНИЕ ПОСЛЕ ОРИГИНАЛЬНОГО ВОПРОСА ОБНОВЛЕНИЕ

Предоставляет ли это то, что вы хотите:

runBlocking {
    supervisorScope {
        try {
            val a = async {
                delay(1000)
                println("Done after delay")
            }
            val b = async { throw Exception() }
            awaitAll(a, b)
        } catch (e: Exception) {
            println("Caught $e")
            // Optional next line, depending on whether you want the async with the delay in it to be cancelled.
            coroutineContext.cancelChildren()
        }
    }
}

Это взято из этого комментария, в котором обсуждается параллельная декомпозиция.

...