Spec-Zone.ru › Kotlin 1.6

Обработка исключений в сопрограммах

В этом разделе рассматривается обработка исключений и отмена по исключениям. Мы уже знаем, что отменённая сопрограмма выбрасывает CancellationException в точках приостановки, и что эта сопрограмма игнорируется механизмом сопрограмм. Здесь мы рассмотрим, что происходит, если исключение выбрасывается во время отмены или несколько дочерних элементов той же сопрограммы выбрасывают исключение.

Распространение исключений

Блоки-создатели сопрограмм бывают двух типов: автоматически обрабатывающие исключения (launch и actor) или предоставляющие их пользователю (async и produce). Когда эти блоки-создатели используются для создания корневой сопрограммы, которая не является дочерней сопрограммой другой сопрограммы, первые блоки-создатели обрабатывают исключения как необработанные, аналогично Thread.uncaughtExceptionHandler в Java, в то время как последние полагаются на пользователя для обработки конечного исключения, например, с помощью await или receive (produce и receive рассматриваются в разделе Каналы).

Это можно продемонстрировать на простом примере, который создаёт корневые сопрограммы с использованием GlobalScope:

GlobalScope — это чувствительный API, который может привести к нежелательным последствиям в нетривиальных ситуациях. Создание корневой сопрограммы для всего приложения — это одно из редких законных применений GlobalScope, поэтому вы должны явно разрешить использование GlobalScope с @OptIn(DelicateCoroutinesApi::class).

import kotlinx.coroutines.*

@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")
    }
}

Полный код вы можете найти здесь.

Вывод этого кода (с отладкой):

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 не используется для дочерних сопрограмм.

В этих примерах CoroutineExceptionHandler всегда устанавливается для сопрограммы, созданной в GlobalScope. Не имеет смысла устанавливать обработчик исключений для сопрограммы, запущенной в рамках основного runBlocking, поскольку основная сопрограмма всегда будет отменена, когда её дочерний элемент завершится с исключением, независимо от установленного обработчика.

Исходное исключение обрабатывается родителем только после завершения всех дочерних элементов, что демонстрируется следующим примером.

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

Полный код можно найти здесь.

Примечание: этот код будет работать правильно только на JDK7+ который поддерживает suppressed исключения

Вывод этого кода:

CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]

Обратите внимание, что этот механизм в настоящее время работает только на Java версии 1.7+. Ограничения для JS и Native — временные и будут сняты в будущем.

Исключения отмены прозрачны и по умолчанию раскрываются:

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. Он похож на обычную задачу, за исключением того, что отмена распространяется только вниз. Это легко продемонстрировать на следующем примере:

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
Последнее изменение: 04 апреля 2022
Каналы Общая изменённая переменная и конкурентность

© 2010–2022 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/exception-handling.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API