如果lifecycleScope 是supervisor,为什么它的子协程失败导致应用程序崩溃?

Yam*_*ion 7 android kotlin kotlin-coroutines

我是 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() }
}
Run Code Online (Sandbox Code Playgroud)

这里一切都很好,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
Run Code Online (Sandbox Code Playgroud)

但后来我为 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)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

输出是意外的,因为Coroutine #2由于应用程序崩溃而未完成其工作。

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)
Run Code Online (Sandbox Code Playgroud)

虽然lifecycleScope.coroutineContextSupervisorJob() + Dispatchers.Main.immediate,在这里我看到子协程的失败影响了父母和其他孩子。

那么监督的目的是lifecycleScope什么?

Mar*_*nik 7

如果你仔细看看你的输出:

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)
Run Code Online (Sandbox Code Playgroud)

这是来自 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")
}
Run Code Online (Sandbox Code Playgroud)

如果我注释掉该setUncaughtExceptionHandler调用,我会像您一样遇到应用程序崩溃。但有了这个,我就在日志中得到一行。

当然,您不会在生产中编写它,但如果您将协程异常处理程序添加到作用域,它将具有相同的效果。

不过,整个故事对我来说并没有多大意义,而且我认为一般而言,异常处理仍然是 Kotlin 协程中需要完善的领域。


Pav*_*ngh 4

有几件事在您的用例中发挥着重要作用

这里一切都很好,协程 #1 失败不会影响父级,也不会影响协程 #2。这就是监督的目的

  1. CoroutineExceptionHandlerThread.uncaughtExceptionHandler

CoroutineExceptionHandler 是默认处理程序,一旦协程抛出异常,它将打印异常详细信息。使用launchjoin将强制协程等待作业完成,这就是为什么您能够看到两个协程的输出

现在,如果协程因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。

不带 join 的 CoroutineExceptionHandler:默认情况下,CoroutineExceptionHandler将忽略CancellationException,如果您不使用join,则它不会打印任何内容。

CoroutineExceptionHandler with join:如果您在协程上使用join,那么构建器将抛出异常CancellationException,并且由于作业尚未完成(其他协程仍在进行中),因此它将打印错误并继续其他作业。

supervisorScope.coroutineContext[Job]!!.children.forEach { it.join() }
Run Code Online (Sandbox Code Playgroud)

遵循与异常传播相同的行为定义,其中GlobalScope没有关联Job对象。

在 Android 中,Thread.uncaughtExceptionHandler是默认处理程序,如果出现未捕获的异常,它将终止应用程序并显示崩溃对话框。

这就是在不同的生态系统中处理有或没有异常的区别join,因此你在 kotlin 测试中不会得到终止行为join(这不在 Android 应用程序中)

虽然lifecycleScope.coroutineContext是SupervisorJob() + Dispatchers.Main.immediate,但在这里我看到子协程的失败影响了父协程和其他子协程。

  1. 不,孩子不会影响父协程,因为根本没有孩子。您的两个协程将在与各个父协程相同的线程上执行,并且不存在父子关系(在协程中使用 Thread.currentThread()?.name 来查看线程名称),因此在发生异常时,父协程将委托android 的异常uncaughtExceptionHandler会杀死应用程序(参考第 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()
            }
        }
Run Code Online (Sandbox Code Playgroud)

或者为了建立父子关系,使用相同的范围来调用子协程

   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")
    }
Run Code Online (Sandbox Code Playgroud)

一旦建立了父子关系,上面的代码就可以处理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
Run Code Online (Sandbox Code Playgroud)

您可以通过删除进一步简化您的示例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")
}
Run Code Online (Sandbox Code Playgroud)

您可以为未捕获的异常实现自己的处理程序,以避免应用程序崩溃(除非您确实需要它,否则不要这样做,因为这是不好的做法,会导致技术债务)。

需要处理未捕获的异常并发送日志文件