android - 如果lifecycleScope是主管,为什么它的子协程失败导致应用程序崩溃?
问题描述
我是 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
由于应用程序崩溃而没有完成它的工作。
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
什么?
解决方案
在您的用例中扮演重要角色的东西很少
在这里一切都很好,协程#1 的失败不会影响父进程,也不会影响协程#2。这就是监督的目的
CoroutineExceptionHandler 是默认处理程序,一旦协程抛出异常,它将打印异常详细信息。使用launch
with 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 without join:默认情况下,CoroutineExceptionHandler会忽略CancellationException
,如果你不使用join
它就不会打印任何东西。
CoroutineExceptionHandler with join:如果你在 coroutine 上使用join,那么 builder 将抛出CancellationException
并且由于作业尚未完成(其他协程仍在进行中),那么它将打印错误并继续其他作业。
supervisorScope.coroutineContext[Job]!!.children.forEach { it.join() }
遵循与GlobalScope没有关联对象的异常传播相同的行为定义。Job
在 Android 中,Thread.uncaughtExceptionHandler是默认处理程序,它会在未捕获异常的情况下终止应用程序并显示崩溃对话框。
这就是在不同生态系统中处理有或没有异常之间的区别,join
因此您在 kotlin 测试中没有终止行为join
(这不在 android 应用程序中)
虽然lifecycleScope.coroutineContext 是SupervisorJob() + Dispatchers.Main.immediate,但在这里我看到子协程的失败影响了父级和其他子级。
- 不,孩子没有影响父母协程,因为根本没有孩子。您的两个协程将与单个父协程在同一线程上执行,并且没有父子关系(在协程中使用 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()
}
}
或者为了建立父子关系,使用相同的范围来调用子协程
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")
}
您可以为未捕获的异常实现自己的处理程序,以避免应用程序崩溃(除非您真的需要,否则不要这样做,因为这是不好的做法,会导致技术债务)。
推荐阅读
- csv - LabVIEW - 将来自多个 DAQ 助手的数据写入同一个 .csv 文件
- jenkins - 如何在环境中存储“复杂”变量?
- javascript - 如何在angularjs中单击时隐藏和显示按钮?
- c# - 绑定到 UserControl 中的元素
- python - 递归神经网络 ValueError: 找到暗淡为 3 的数组。估计器预期 <= 2
- java - Apache Camel:带有重音字符的文件处理
- c# - Polly:如何结合 TimeoutPolicy 和 RetryPolicy 来请求 Func
- file - ZIP 文件规范加密标头
- c# - 如何首先在 Xamarin.Android 中以编程方式连接到特定的 SSID?
- javascript - 重叠图像预览