Если lifecycleScope является супервизором, почему отказ его дочерней функции вызывает приложение cra sh? - PullRequest
4 голосов
/ 05 февраля 2020

Я новичок в Kotlin сопрограмм и пытаюсь понять надзор. Как говорят в документах:

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


Хорошо, Я написал следующий код для JVM:

@JvmStatic
fun main(args: Array<String>) = runBlocking {
    val supervisorScope = CoroutineScope(Dispatchers.Default + SupervisorJob())

    // Coroutine #1
    supervisorScope.launch {
        println("Coroutine #1 start")
        delay(100)
        throw RuntimeException("Coroutine #1 failure")
    }

    // Coroutine #2
    supervisorScope.launch {
        for (i in 0 until 5) {
            println("Coroutine #2: $i")
            delay(100)
        }
    }

    supervisorScope.coroutineContext[Job]!!.children.forEach { it.join() }
}

Здесь все хорошо, сбой Coroutine #1 не влияет ни на родителя, ни на Coroutine #2. Это цель надзора. Вывод соответствует документам:

Coroutine #1 start
Coroutine #2: 0
Coroutine #2: 1
Exception in thread "DefaultDispatcher-worker-1" java.lang.RuntimeException: Coroutine #1 failure
    at supervisor.SupervisorJobUsage$main$1$1.invokeSuspend(SupervisorJobUsage.kt:16)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:561)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:727)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:667)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:655)
Coroutine #2: 2
Coroutine #2: 3
Coroutine #2: 4

Process finished with exit code 0

Но тогда я написал почти тот же код для Android:

class CoroutineJobActivity : AppCompatActivity() {

    private val TAG = "CoroutineJobActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        testSupervisorScope()
    }

    private fun testSupervisorScope() {
        // Coroutine #1
        lifecycleScope.launch(Dispatchers.Default) {
            Log.d(TAG, "testSupervisorScope: Coroutine #1 start")
            delay(100)
            throw RuntimeException("Coroutine #1 failure")
        }

        // Coroutine #2
        lifecycleScope.launch(Dispatchers.Default) {
            for (i in 0 until 5) {
                Log.d(TAG, "testSupervisorScope: Coroutine #2: $i")
                delay(100)
            }
        }
    }
}

Вывод неожиданный, потому что Coroutine #2 не заканчивается sh его работа из-за приложения cra sh.

testSupervisorScope: Coroutine #1 start
testSupervisorScope: Coroutine #2: 0
testSupervisorScope: Coroutine #2: 1
testSupervisorScope: Coroutine #2: 2
FATAL EXCEPTION: DefaultDispatcher-worker-2
    Process: jp.neechan.kotlin_coroutines_android, PID: 23561
    java.lang.RuntimeException: Coroutine #1 failure
        at jp.neechan.kotlin_coroutines_android.coroutinejob.CoroutineJobActivity$testSupervisorScope$1.invokeSuspend(CoroutineJobActivity.kt:25)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:561)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:727)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:667)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:655)

Хотя lifecycleScope.coroutineContext равно SupervisorJob() + Dispatchers.Main.immediate, здесь я видите, что отказ от сопрограммы ребенка затронул родителей и других детей.

Так, какова цель контроля lifecycleScope?

Ответы [ 3 ]

1 голос
/ 05 февраля 2020

Есть несколько вещей, которые играют важную роль в вашем случае использования

Здесь все хорошо, сбой Coroutine # 1 не влияет ни на родительский элемент, ни на Coroutine # 2. Это цель наблюдения

  1. CoroutineExceptionHandler против Thread.uncaughtExceptionHandler

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

Теперь, если сопрограмма потерпела крах с присоединиться , тогда он выдаст CancellationException

В частности, это означает, что a parent coroutine invoking join on a child coroutine that was started using launch(coroutineContext) { ... } builder throws CancellationException if the child had crashed, если в контексте не установлен нестандартный CoroutineExceptionHandler .

CoroutineExceptionHandler без объединения : по умолчанию CoroutineExceptionHandler будет игнорировать CancellationException, а если вы не используете join, то оно выиграет ' не печатать ничего.

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

supervisorScope.coroutineContext[Job]!!.children.forEach { it.join() }

Следуйте тому же поведению, определенному с помощью Распространение исключений , где GlobalScope не имеет связанного Job объекта.

В Android, Thread.uncaughtExceptionHandler - это обработчик по умолчанию, который убьет приложение в случае необработанного исключения и покажет диалоговое окно cra sh.

Разница между обработкой исключений с * или без join в разных экосистемах, следовательно, вы не получаете поведения завершения в своем тесте kotlin с join (которого нет в приложении android)

Хотя lifecycleScope.coroutineContext имеет значение SupervisorJob () + Dispatchers Главное, здесь я вижу, что отказ от сопрограммы ребенка затронул родителей и других детей.

Нет, ребенок не влияет на родительскую сопрограмму , потому что ребенка вообще нет. Обе ваши сопрограммы будут выполняться в том же потоке, что и отдельные родительские сопрограммы, и в ваших сопрограммах отсутствует отношение «родитель-потомок» (используйте Thread.currentThread () ?. name для просмотра имени потока), поэтому в случае исключения родительский объект делегирует исключение из uncaughtExceptionHandler из android, которое убивает приложение (см. пункт 1).

Итак, вы можете использовать withContext

lifecycleScope.launch(Dispatchers.Default) {
            for (i in 0 until 5) {
                Log.d(TAG, "testSupervisorScope: Coroutine #1: $i")
                delay(100)
            }

            try {
                // can use another context to change thread, e.g Dispatchers.IO
                withContext(lifecycleScope.coroutineContext) {
                    Log.d(TAG, "testSupervisorScope: Coroutine withContext start")
                    delay(100)
                    throw RuntimeException("Coroutine sub-task failure")
                }

            } catch (e: java.lang.RuntimeException) {
                e.printStackTrace()
            }
        }

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

   private fun testSupervisorScope() = runBlocking {
        // Coroutine #1
        lifecycleScope.launch(Dispatchers.Default) {
            for (i in 0 until 5) {
                Log.d(TAG, "testSupervisorScope: Coroutine #1: $i")
                delay(100)
            }


            // Coroutine child #1
            try {
                childCoroutineWithException().await()
            } catch (e: Exception) {
                Log.d(TAG, "caught exception")
                e.printStackTrace()
            }
        }
    }

    // Note: use same scope `lifecycleScope` to ceate child coroutine to establish parent-child relation
    fun childCoroutineWithException(): Deferred<String> = lifecycleScope.async {
        Log.d(TAG, "testSupervisorScope: Coroutine child #1 start")
        delay(100)
        throw RuntimeException("Coroutine child #1 failure")
    }

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

Результат с дочерними сопрограммами:

CoroutineJobActivity: testSupervisorScope: Coroutine #1: 1
CoroutineJobActivity: testSupervisorScope: Coroutine #1: 2
CoroutineJobActivity: testSupervisorScope: Coroutine #1: 3
CoroutineJobActivity: testSupervisorScope: Coroutine #1: 4
CoroutineJobActivity: testSupervisorScope: Coroutine #1: 5
CoroutineJobActivity: testSupervisorScope: Coroutine child #1 start
CoroutineJobActivity: Coroutine child #1 failure

Вы можете еще больше упростить свой пример, удалив runBlocking

private fun testSupervisorScope(){
    // Coroutine #1
    lifecycleScope.launch(Dispatchers.Default) {
        for (i in 0 until 5) {
            Log.d(TAG, "testSupervisorScope: Coroutine #1: $i")
            try {
                childCoroutineWithException().await()
            } catch (e: Exception) {
                Log.d(TAG, "caught exception")
                e.printStackTrace()
            }
            delay(100)
        }

    }
}

// Note: use same scope `lifecycleScope` to ceate child coroutine to establish parent-child relation
fun childCoroutineWithException(): Deferred<String> = lifecycleScope.async {
    Log.d(TAG, "testSupervisorScope: Coroutine child #1 start")
    delay(100)
    throw RuntimeException("Coroutine child #1 failure")
}

Вы можете реализовать свой собственный обработчик для необработанных исключений, чтобы избежать app cra sh с помощью (Не делайте этого, если вам это действительно не нужно, потому что это b рекламная практика, причины Техническая задолженность ).

Необходимо обработать необработанное исключение и отправить файл журнала

1 голос
/ 05 февраля 2020

Если вы внимательно посмотрите на свои выходные данные:

Exception in thread "DefaultDispatcher-worker-1" java.lang.RuntimeException: Coroutine #1 failure
    at supervisor.SupervisorJobUsage$main$1$1.invokeSuspend(SupervisorJobUsage.kt:16)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:561)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:727)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:667)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:655)

Это отчет обработчика необработанных исключений уровня JVM. Это означает, что, хотя это и не отменило работу вашей области, исключение прервало поток Java. Исполнитель может легко исправить такие ошибки, но Android использует другой необработанный обработчик исключений, который сразу убивает все приложение. Ничто в области сопрограмм не меняет это поведение.

Вот некоторый код, который вы можете попробовать, чтобы увидеть этот механизм в действии:

GlobalScope.launch(Dispatchers.Default) {
    Thread.currentThread().setUncaughtExceptionHandler { thread, exception ->
        Log.e("MyTag", "We got an error on ${thread.name}: $exception")
    }
    throw RuntimeException("Dead")
}

Если я закомментирую вызов setUncaughtExceptionHandler, я получу приложение Cra sh, как вы. Но с этим на месте я просто получаю строку в журнале.

Конечно, вы не напишите это в работе, но если вы добавите обработчик исключений сопрограммы в область действия, он будет иметь то же самое эффект.

Вся эта история не имеет для меня большого смысла, хотя, и я думаю, что обработка исключений в целом все еще является областью, которая нуждается в полировке в Kotlin сопрограммах.

0 голосов
/ 05 февраля 2020

Проблема в том, что SupervisorJob не работает так, как вы ожидаете. Идея SupervisorScope состоит в том, что когда исключение запускается одним из его дочерних элементов, оно не отменяет выполнение других дочерних элементов, но если исключение не CancellationException, оно распространяется на исключение в системе, и если вы не используете не уловить, приложение будет Crack sh. Другой метод управления исключением - передать в область действия CoroutineExceptionHandler, которая должна управлять исключением, запущенным дочерним элементом.

...