Родительские сопрограммы не отменяют другие дочерние сопрограммы после исключения

Если в области нет SupervisorJob(), то сбой одного дочернего элемента сопрограммы приведет к сбою родительской области, а все остальные дочерние элементы родительской области завершатся ошибкой.

Если приведенное выше утверждение верно, то в примере кода child 2 coroutine не следует печатать, поскольку первая сопрограмма завершается сбоем из-за выданного исключения.

Но я все еще получаю этот вывод:

child 1 coroutine
child 2 coroutine

В чем разница при использовании SupervisorJob с val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())?

val scope = CoroutineScope(Dispatchers.IO)

Button(onClick = {
    scope.launch {
        launch {
            println("child 1 coroutine")
            delay(2.seconds)
            throw CancellationException()
        }
        delay(4.seconds)
        launch {
            println("child 2 coroutine")
        }
    }
}) {
    Text(text = "Scope check")
}

ОТВЕТ: Для лучшего объяснения прочитайте ниже ответ, который отмечен как правильный от @Leviathan.

Код Crt для обработки исключения

val handler = CoroutineExceptionHandler { _, e ->
                    println("handled: ${e.message}")
                }
                val scope = CoroutineScope(Dispatchers.IO + handler)
                Button(
                    modifier = Modifier.padding(innerPadding),
                    onClick = {
                        scope.launch {
                            supervisorScope {
                                launch {
                                    println("child 1 coroutine")
                                    delay(1.seconds)
                                    throw Exception("Child 1 Exception")
                                }
                                delay(4.seconds)
                                launch {
                                    println("child 2 coroutine")
                                }
                            }

                        }
                    }) {
                    Text(text = "Scope check")
                }

просто создайте обычное исключение и проверьте

Raghunandan 26.08.2024 08:16

@Raghunandan, если я выдаю обычное исключение, выбрасываю Exception («Сбой в дочернем элементе 1»), то приложение аварийно завершает работу. После попытки поймать тот же вывод

PRATHIV 26.08.2024 09:01

fun main() { runBlocking { launch { println("дочерняя сопрограмма 1") Delay(2000) throw Exception() } Delay(4000) launch { println("дочерняя сопрограмма 2") } } } попробуйте это на игровой площадке Kotlin. если первый ребенок выдает исключение, второй отменяется

Raghunandan 26.08.2024 09:52
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
4
50
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Без SupervisorJob: сбой дочерней сопрограммы (например, дочерней 1) приведет к отмене родительской области, и все остальные дочерние сопрограммы (например, дочерняя 2) также будут отменены.

С SupervisorJob: сбой дочерней сопрограммы не влияет на другие дочерние сопрограммы.

Потому что: Job гарантирует, что в случае сбоя дочерней сопрограммы другие дочерние сопрограммы не будут отменены.

Без SupervisorJob по-прежнему печатается дочерний элемент 2, я обновил SS в QA, пожалуйста, проверьте

PRATHIV 26.08.2024 09:09

Сначала немного о CancellationException — это особое исключение и по сути означает отмену сопрограммы, а не ее сбой. Когда сопрограмма отменяется, она завершается, но не отменяет своего родителя. throw CancellationException() поскольку последняя строка в сопрограмме ничего не дает.

Теперь о разнице между использованием SupervisorJob() и неиспользованием. Используя scope.launch, вы создаете новую сопрограмму с новым контекстом сопрограммы и новым заданием, которое отменяется при сбое одного из его дочерних элементов. Итак, в вашем примере на самом деле не имеет значения, есть ли scopeSupervisorJob или нет, потому что все, что он видит, - это одна неисправная дочерняя сопрограмма, созданная launch.

Вероятно, вам нужно запускать дочерние сопрограммы в самом scope. В приостановленной функции вы можете заменить scope.launch на with(scope){, scope.run{ или просто supervisorScope {. В вашем случае вы можете увидеть разницу, обернув сопрограммы в контролируемую область сопрограммы внутри launch:

val handler = CoroutineExceptionHandler { _, exception ->
    println("Handler got $exception")
}
//----
    val scope = rememberCoroutineScope()
    Button(onClick = {
        scope.launch {
            CoroutineScope(SupervisorJob() + handler).run {
            // OR
            // CoroutineScope(handler).run {
                launch {
                    println("child 1 coroutine")
                    delay(2.seconds)
                    throw RuntimeException()
                }
                delay(4.seconds)
                launch {
                    println("child 2 coroutine")
                }
            }
        }
    }) {
        Text(text = "Scope check")
    }

Подробнее читайте в этой статье.

Ответ принят как подходящий

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

  • Родительская сопрограмма/задание: сопрограмма/задание, созданное scope.launch
  • Первая сопрограмма/задание: первая дочерняя сопрограмма/задание, созданное launch, выдающее исключение.
  • Вторая сопрограмма/задание: вторая дочерняя сопрограмма/задание, созданное launch

Следующие заблуждения мешают вашему коду вести себя должным образом:

  1. Добавление CancellationException просто отменяет текущую работу и останавливает ее. Отмена распространяется только вниз по иерархии должностей, а не вверх. Вот почему родительское задание не видит неудачную сопрограмму. Поэтому вторая сопрограмма не затрагивается и завершается успешно.

    Итак, что вам на самом деле нужно в вашем примере кода, так это сбой сопрограммы. Для этого вам нужно заменить CancellationException любым другим исключением, например Exception().

  2. Вторая сопрограмма запускается только после delay(4.seconds): если первая сопрограмма дает сбой через две секунды, вторая сопрограмма еще даже не запускается. Это можно легко исправить, переместив 4-секундную задержку внутри блока запуска второй сопрограммы. Ваш пример кода теперь должен выглядеть так:

    scope.launch {
        launch {
            println("child 1 coroutine")
            delay(2.seconds)
            throw Exception("child 1 failed")
        }
    
        launch {
            delay(4.seconds)
            println("child 2 coroutine")
        }
    }
    

    Теперь происходит то, что обе сопрограммы запускаются немедленно. Через две секунды первая сопрограмма завершается с ошибкой. Поскольку вы (еще) не указали SupervisorJob, неудачная дочерняя сопрограмма приводит к сбою родительской сопрограммы. И это приводит к отмене всех оставшихся дочерних сопрограмм, в данном случае второй сопрограммы. Это приводит к ожидаемому поведению: child 2 coroutine никогда не печатается.

  3. Когда вы затем измените область действия на использование SupervisorJob, вы можете подумать, что теперь будет напечатан child 2 coroutine. Однако это не так: SupervisorJob защищает своих прямых дочерних элементов только в случае сбоя дочернего объекта. Поскольку вы предоставили SupervisorJob в область, защищены только сопрограммы, запущенные в этой области. Однако первая и вторая дочерние сопрограммы запускаются в области родительской сопрограммы, поэтому родительским для них является это задание, а не SupervisorJob. Исправить это можно любым из следующих вариантов:

    • Также запустите две дочерние сопрограммы с помощью scope.launch.
    • Вместо этого оберните две дочерние сопрограммы в supervisorScope { /*...*/ }.
    • Вместо этого назначьте SupervisorJob переменной (val supervisor = SupervisorJob()) и предоставьте ее в качестве контекста для двух дочерних сопрограмм: launch(supervisor) { /*...*/ }
  4. Хотя сбой первой сопрограммы больше не приводит к сбою родительской сопрограммы, поэтому вторая сопрограмма продолжает работать, фактическое исключение так и не было обработано. А поскольку это необработанное исключение поднимает иерархию заданий и никогда не находит обработчика, самое верхнее задание завершается с ошибкой, и все сопрограммы в иерархии отменяются, включая задание супервизора и, следовательно, также вторую сопрограмму. А затем необработанное исключение генерируется повторно, и ваше приложение аварийно завершает работу.

    Чего вам не хватает, так это CoroutineExceptionHandler, как описано в документации SupervisorJob:

    Сбой или отмена дочернего элемента не приводит к сбою задания супервизора и не влияет на другие его дочерние элементы, поэтому супервизор может реализовать специальную политику для обработки сбоев своих дочерних элементов:

    • Сбой дочернего задания, созданного с помощью launch, можно обработать с помощью CoroutineExceptionHandler в контексте.

    [...]

    Предположим, вы используете этот обработчик:

    val handler = CoroutineExceptionHandler { _, e ->
        println("handled: ${e.message}")
    }
    

    Затем вы можете предоставить обработчик в качестве контекста для всей области:

    val scope = CoroutineScope(Dispatchers.IO + handler)
    

    Или вы можете указать его только для родительской сопрограммы:

    scope.launch(handler) { /*...*/ }
    

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

И только теперь вы получите ожидаемый результат:

child 1 coroutine
handled: child 1 failed
child 2 coroutine

Хорошее и понятное объяснение, но можете ли вы объяснить, получаем ли мы пользу, когда супервизорJob добавляется в coroutineScope(), с каким-нибудь примером, пожалуйста

PRATHIV 27.08.2024 06:38

Другие вопросы по теме