Оглавление
Обработка исключений
Этот раздел охватывает обработку исключений и отмену при возникновении исключений. Мы уже знаем, что отменённая корутина выбрасывает CancellationException в точках приостановки, и что она игнорируется механизмом корутин. Здесь мы рассмотрим, что происходит, если исключение возникает во время отмены или несколько дочерних корутин одного и того же корня выбрасывают исключение.
Распространение исключений
Конструкторы корутин бывают двух типов: автоматически обрабатывающие исключения (launch и actor) или предоставляющие их пользователю (async и produce). Когда эти конструкторы используются для создания корневой корутины, не являющейся дочерней корутиной другой корутины, первые обрабатывают исключения как необработанные исключения, аналогично Java Thread.uncaughtExceptionHandler, тогда как вторые полагаются на пользователя для обработки конечного исключения, например, с помощью await или receive (produce и receive будут рассмотрены позже в разделе Каналы).
Это можно продемонстрировать на простом примере, создающем корневые корутины с использованием GlobalScope:
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = GlobalScope.launch { // root coroutine with launch
println("Throwing exception from launch")
throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
}
job.join()
println("Joined failed job")
val deferred = GlobalScope.async { // root coroutine with async
println("Throwing exception from async")
throw ArithmeticException() // Nothing is printed, relying on user to call await
}
try {
deferred.await()
println("Unreached")
} catch (e: ArithmeticException) {
println("Caught ArithmeticException")
}
}
Полный код можно найти здесь.
Вывод этого кода (с отладкой):
Throwing exception from launch Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException Joined failed job Throwing exception from async Caught ArithmeticException
CoroutineExceptionHandler
Можно настроить стандартное поведение вывода необработанных исключений в консоль. Элемент контекста CoroutineExceptionHandler в корневой корутине может быть использован как общий catch блок для этой корутины и всех её дочерних корутин, где может выполняться пользовательская обработка исключений. Это аналогично Thread.uncaughtExceptionHandler. Вы не можете восстановить от исключения в CoroutineExceptionHandler. Корутина уже завершилась с соответствующим исключением, когда вызывается обработчик. Обычно обработчик используется для регистрации исключения, отображения сообщения об ошибке, завершения и/или перезапуска приложения.
В JVM можно переопределить глобальный обработчик исключений для всех корутин, зарегистрировав CoroutineExceptionHandler через ServiceLoader. Глобальный обработчик исключений подобен Thread.defaultUncaughtExceptionHandler, который используется, когда не зарегистрированы более специфичные обработчики. В Android uncaughtExceptionPreHandler установлен в качестве глобального обработчика исключений корутин.
CoroutineExceptionHandler вызывается только для необработанных исключений — исключений, которые не обрабатывались каким-либо другим способом. В частности, все дочерние корутины (корутины, созданные в контексте другой Job) делегируют обработку своих исключений родительской корутине, которая также делегирует родительской и так далее до корня, поэтому CoroutineExceptionHandler установленный в их контексте никогда не используется. Кроме того, конструктор async всегда перехватывает все исключения и представляет их в результирующем объекте Deferred, поэтому его CoroutineExceptionHandler также не имеет эффекта.
Корутины, работающие в области наблюдения, не распространяют исключения своему родителю и исключаются из этого правила. Подробности см. в разделе Наблюдение данного документа.
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope
throw AssertionError()
}
val deferred = GlobalScope.async(handler) { // also root, but async instead of launch
throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
}
joinAll(job, deferred)
//sampleEnd
}
Полный код можно найти здесь.
Вывод этого кода:
CoroutineExceptionHandler got java.lang.AssertionError
Отмена и исключения
Отмена тесно связана с исключениями. Корутины используют CancellationException для отмены, эти исключения игнорируются всеми обработчиками, поэтому их следует использовать только как источник дополнительной отладочной информации, которая может быть получена с помощью catch блока. Когда корутина отменяется с помощью Job.cancel, она завершается, но не отменяет родительскую корутину.
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val job = launch {
val child = launch {
try {
delay(Long.MAX_VALUE)
} finally {
println("Child is cancelled")
}
}
yield()
println("Cancelling child")
child.cancel()
child.join()
yield()
println("Parent is not cancelled")
}
job.join()
//sampleEnd
}
Полный код можно найти здесь.
Вывод этого кода:
Cancelling child Child is cancelled Parent is not cancelled
Если корутина сталкивается с исключением, отличным от CancellationException, она отменяет родительскую корутину с этим исключением. Это поведение не может быть переопределено и используется для обеспечения стабильных иерархий корутин при структурированной конкурентности. CoroutineExceptionHandler не используется для дочерних корутин.
В этих примерах CoroutineExceptionHandler всегда установлен для корутины, созданной в GlobalScope. Нет смысла устанавливать обработчик исключений для корутины, запущенной в области основной runBlocking, так как главная корутина будет всегда отменена, когда её дочерняя завершится с исключением, несмотря на установленный обработчик.
Исходное исключение обрабатывается родителем только после завершения всех дочерних корутин, что демонстрируется следующим примером.
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
launch { // the first child
try {
delay(Long.MAX_VALUE)
} finally {
withContext(NonCancellable) {
println("Children are cancelled, but exception is not handled until all children terminate")
delay(100)
println("The first child finished its non cancellable block")
}
}
}
launch { // the second child
delay(10)
println("Second child throws an exception")
throw ArithmeticException()
}
}
job.join()
//sampleEnd
}
Полный код можно найти здесь.
Вывод этого кода:
Second child throws an exception Children are cancelled, but exception is not handled until all children terminate The first child finished its non cancellable block CoroutineExceptionHandler got java.lang.ArithmeticException
Агрегирование исключений
Когда несколько дочерних корутин завершаются с исключением, общее правило — "первое исключение выигрывает", то есть обрабатывается первое исключение. Все дополнительные исключения, возникшие после первого, прикрепляются к первому как подавленные.
import kotlinx.coroutines.*
import java.io.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}")
}
val job = GlobalScope.launch(handler) {
launch {
try {
delay(Long.MAX_VALUE) // it gets cancelled when another sibling fails with IOException
} finally {
throw ArithmeticException() // the second exception
}
}
launch {
delay(100)
throw IOException() // the first exception
}
delay(Long.MAX_VALUE)
}
job.join()
}
Вы можете получить полный код здесь.
Примечание: этот код будет работать корректно только на JDK7+, который поддерживает
suppressedисключения
Вывод этого кода:
CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]
Обратите внимание, что этот механизм в настоящее время работает только на Java версии 1.7+. Ограничения для JS и Native носят временный характер и будут сняты в будущем.
Исключения отмены прозрачны и по умолчанию распаковываются:
import kotlinx.coroutines.*
import java.io.*
fun main() = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
val inner = launch { // all this stack of coroutines will get cancelled
launch {
launch {
throw IOException() // the original exception
}
}
}
try {
inner.join()
} catch (e: CancellationException) {
println("Rethrowing CancellationException with original cause")
throw e // cancellation exception is rethrown, yet the original IOException gets to the handler
}
}
job.join()
//sampleEnd
}
Вы можете получить полный код здесь.
Вывод этого кода:
Rethrowing CancellationException with original cause CoroutineExceptionHandler got java.io.IOException
Наблюдение
Как мы изучали ранее, отмена — это двунаправленные отношения, распространяющиеся по всей иерархии задач. Давайте рассмотрим случай, когда требуется однонаправленная отмена.
Хороший пример такого требования — элемент пользовательского интерфейса с задачей, определённой в его области. Если какая-либо из дочерних задач пользовательского интерфейса завершилась с ошибкой, то не всегда необходимо отменять (фактически убивать) весь элемент пользовательского интерфейса. Но если элемент пользовательского интерфейса уничтожен (и его задача отменена), то необходимо завершить все дочерние задачи, так как их результаты больше не нужны.
Другой пример — процесс сервера, который запускает несколько дочерних задач и должен наблюдать за их выполнением, отслеживая ошибки и перезапуская только ошибочные.
Задача наблюдения
Для этих целей можно использовать SupervisorJob. Она похожа на обычную Job с единственным исключением: отмена распространяется только вниз. Это можно легко продемонстрировать на следующем примере:
import kotlinx.coroutines.*
fun main() = runBlocking {
val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)) {
// launch the first child -- its exception is ignored for this example (don't do this in practice!)
val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) {
println("The first child is failing")
throw AssertionError("The first child is cancelled")
}
// launch the second child
val secondChild = launch {
firstChild.join()
// Cancellation of the first child is not propagated to the second child
println("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active")
try {
delay(Long.MAX_VALUE)
} finally {
// But cancellation of the supervisor is propagated
println("The second child is cancelled because the supervisor was cancelled")
}
}
// wait until the first child fails & completes
firstChild.join()
println("Cancelling the supervisor")
supervisor.cancel()
secondChild.join()
}
}
Вы можете получить полный код здесь.
Вывод этого кода:
The first child is failing The first child is cancelled: true, but the second one is still active Cancelling the supervisor The second child is cancelled because the supervisor was cancelled
Область наблюдения
Вместо coroutineScope, мы можем использовать supervisorScope для областного конкурентного выполнения. Она распространяет отмену только в одном направлении и отменяет все свои дочерние задачи только в случае собственной ошибки. Она также ожидает завершения всех дочерних задач, так же как и coroutineScope.
import kotlin.coroutines.*
import kotlinx.coroutines.*
fun main() = runBlocking {
try {
supervisorScope {
val child = launch {
try {
println("The child is sleeping")
delay(Long.MAX_VALUE)
} finally {
println("The child is cancelled")
}
}
// Give our child a chance to execute and print using yield
yield()
println("Throwing an exception from the scope")
throw AssertionError()
}
} catch(e: AssertionError) {
println("Caught an assertion error")
}
}
Вы можете получить полный код здесь.
Вывод этого кода:
The child is sleeping Throwing an exception from the scope The child is cancelled Caught an assertion error
Исключения в контролируемых задачах
Другое важное различие между обычными и наблюдаемыми задачами — это обработка исключений. Каждая дочерняя задача должна обрабатывать свои исключения самостоятельно с помощью механизма обработки исключений. Это различие обусловлено тем, что ошибка дочерней задачи не распространяется на родительскую. Это означает, что задачи, запущенные непосредственно внутри supervisorScope, используют CoroutineExceptionHandler, установленный в их области, так же как и корневые задачи (см. раздел CoroutineExceptionHandler для подробностей).
import kotlin.coroutines.*
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
supervisorScope {
val child = launch(handler) {
println("The child throws an exception")
throw AssertionError()
}
println("The scope is completing")
}
println("The scope is completed")
}
Вы можете получить полный код здесь.
Вывод этого кода:
The scope is completing The child throws an exception CoroutineExceptionHandler got java.lang.AssertionError The scope is completed
© 2010–2020 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/reference/coroutines/exception-handling.html