Обработка исключений сопрограмм
В этом разделе рассматривается обработка исключений и отмена при исключениях. Мы уже знаем, что отменённая сопрограмма выбрасывает CancellationException в точках приостановки, и что оно игнорируется механизмом сопрограмм. Здесь мы рассмотрим, что происходит, если исключение выбрасывается во время отмены или несколько дочерних сопрограмм той же сопрограммы выбрасывают исключение.
Распространение исключений
Конструкторы сопрограмм бывают двух типов: автоматически распространяющие исключения (launch и actor) или предоставляющие их пользователю (async и produce). Когда эти конструкторы используются для создания корневой сопрограммы, которая не является дочерней сопрограммой другой сопрограммы, первые конструкторы обрабатывают исключения как необработанные исключения, аналогично Thread.uncaughtExceptionHandler, в то время как последние полагаются на пользователя для обработки конечного исключения, например, с помощью await или receive (produce и receive описаны в разделе Каналы).
Это можно продемонстрировать на простом примере, который создаёт корневые сопрограммы с использованием GlobalScope:
import kotlinx.coroutines.*
//sampleStart
@OptIn(DelicateCoroutinesApi::class)
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")
}
}
//sampleEnd
Вывод этого кода (с отладкой debug):
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 невозможно. Сопрограмма уже завершилась с соответствующим исключением, когда вызывается обработчик. Обычно обработчик используется для регистрации исключения, отображения сообщения об ошибке, завершения и/или перезапуска приложения.
CoroutineExceptionHandler вызывается только при необработанных исключениях — исключениях, которые не обрабатывались каким-либо другим способом. В частности, все дочерние сопрограммы (сопрограммы, созданные в контексте другой Job) делегируют обработку своих исключений родительской сопрограмме, которая также делегирует родительской и так далее до корневой, поэтому CoroutineExceptionHandler установленный в их контексте никогда не используется. Кроме того, конструктор async всегда перехватывает все исключения и представляет их в результирующем объекте Deferred, поэтому его CoroutineExceptionHandler тоже не имеет эффекта.
import kotlinx.coroutines.*
@OptIn(DelicateCoroutinesApi::class)
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 не используется для дочерних сопрограмм.
Исходное исключение обрабатывается родителем только после завершения всех дочерних элементов, что демонстрируется в следующем примере.
import kotlinx.coroutines.*
@OptIn(DelicateCoroutinesApi::class)
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.*
@OptIn(DelicateCoroutinesApi::class)
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()
}
Вывод этого кода:
CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]
Исключения отмены прозрачны и по умолчанию распаковываются:
import kotlinx.coroutines.*
import java.io.*
@OptIn(DelicateCoroutinesApi::class)
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 {
//sampleStart
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()
}
//sampleEnd
}
Вывод этого кода:
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 {
//sampleStart
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")
}
//sampleEnd
}
Вывод этого кода:
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 {
//sampleStart
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")
//sampleEnd
}
Вывод этого кода:
The scope is completing The child throws an exception CoroutineExceptionHandler got java.lang.AssertionError The scope is completed
© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/exception-handling.html