Kotlin协程无法处理异常。

13

我在尝试使用协程时发现了一些奇怪的行为。我想要通过suspendCoroutine()将我的项目中的一些异步请求转换。这里展示了一段代码,显示了这个问题。

在第一个情况下,在runBlocking协程中调用suspend函数时,来自continuation的异常被传递到catch块,然后runBlocking成功完成。但是在第二种情况下,当创建新的async协程时,异常通过catch块并导致整个程序崩溃。

package com.example.lib

import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

object Test {
    fun runSuccessfulCoroutine() {
        runBlocking {
            try {
                Repository.fail()
            } catch (ex: Throwable) {
                println("Catching ex in runSuccessfulCoroutine(): $ex")
            }
        }
    }

    fun runFailingCoroutine() {
        runBlocking {
            try {
                async { Repository.fail() }.await()
            } catch (ex: Throwable) {
                println("Catching ex in runFailingCoroutine(): $ex")
            }
        }
    }
}

object Repository {
    suspend fun fail(): Int = suspendCoroutine { cont ->
        cont.resumeWithException(RuntimeException("Exception at ${Thread.currentThread().name}"))
    }
}


fun main() {
    Test.runSuccessfulCoroutine()
    println()

    Test.runFailingCoroutine()

    println("We will never get here")
}

这就是在控制台上打印的内容:

Catching ex in runSuccessfulCoroutine(): java.lang.RuntimeException: Exception at main

Catching ex in runFailingCoroutine(): java.lang.RuntimeException: Exception at main
Exception in thread "main" java.lang.RuntimeException: Exception at main
    at com.example.lib.Repository.fail(MyClass.kt:32)
    at com.example.lib.Test$runFailingCoroutine$1$1.invokeSuspend(MyClass.kt:22)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
    at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:236)
    at kotlinx.coroutines.EventLoopBase.processNextEvent(EventLoop.kt:123)
    at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:69)
    at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:45)
    at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
    at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:35)
    at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
    at com.example.lib.Test.runFailingCoroutine(MyClass.kt:20)
    at com.example.lib.MyClassKt.main(MyClass.kt:41)
    at com.example.lib.MyClassKt.main(MyClass.kt)

Process finished with exit code 1

有什么想法为什么会发生这种情况 - 是一个bug,还是我使用协程的方式不对?

更新:

runFailingCoroutine() 中使用 coroutineScope { ... } 可以缓解问题。

fun runFailingCoroutine() = runBlocking {
    try {
        coroutineScope { async { fail() }.await()  }
    } catch (ex: Throwable) {
        println("Catching ex in runFailingCoroutine(): $ex")
    }
}
2个回答

8
你的第二个例子的行为是正确的,这是结构化并发的工作原理。由于内部的async块抛出了异常,因此该协程被取消。由于结构化并发,父作业也被取消。
看一下这个小例子:
val result = coroutineScope {
    async {
        throw IllegalStateException()
    }
    10
}

即使我们从未请求async结果,此代码块也永远不会返回值。内部协程将被取消,并且外部范围也将被取消。

如果您不喜欢这种行为,可以使用supervisorScope。在这种情况下,内部协程可能会失败,而不会导致外部协程失败。

val result = supervisorScope {
    async {
        throw IllegalStateException()
    }
    10
}

在你的第一个示例中,你在协程块内捕获了异常,因此协程正常退出。
有关此主题的讨论,请参见:

是的,我完全没有考虑到结构化并发。建议多阅读以下内容: Github问题:使用结构化并发处理异常(rx & async) 以及Roman Elizarov的“结构化并发”Medium文章 - Alexander Sitnikov

6
我昨天遇到了这个问题,这是我的分析
简而言之,这种行为是期望的,因为在 Kotlin 中,async 的目的与其他语言不同。你应该谨慎使用它,仅当你必须将一个任务分解成几个并行运行的子任务时才使用。
每当你只想写
val result = async { work() }.await()

你应该改为写:


val result = withContext(Default) { work() }

这样会按预期方式工作。另外,每当有机会时,您应该将 withContext 调用移到 work() 函数中,并使其成为 suspend fun


2
谢谢,这是有趣的分析。另外,很好的一点是,async { ... }.await() 应该不应该出现在一起。但最初我发现这种行为是因为我需要将3个并行 API 调用压缩成一个对象,而一个 API 调用中的异常会导致整个应用程序崩溃。 - Alexander Sitnikov
2
是的,您必须将三个异步调用放入coroutineScope构建器中,以便您拥有一个单独的作用域,该作用域将被取消。您的主作用域应在其override val coroutineContext中使用SupervisorJob()而不仅仅是Job() - Marko Topolnik
@AlexanderSitnikov,使用GlobalScope.asyinc{ ... }.await()是否可以?为什么完全不应该使用asying { ... }.await() - Mehdi Karamosly

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接