Je jouais avec des coroutines et j'ai trouvé un comportement très étrange. Je souhaite convertir certaines demandes asynchrones dans mon projet à l'aide de suspendCoroutine()
. Voici un morceau de code montrant ce problème.
Dans le premier cas, lorsque la fonction suspend est appelée dans runBlocking
coroutine, l'exception de la continuation va au bloc catch, puis runBlocking
se termine avec succès. Mais dans le deuxième cas, lors de la création d'une nouvelle coroutine async
, une exception passe par le bloc catch et bloque tout le programme.
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")
}
C'est ce qui est imprimé sur la console:
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
Des idées pourquoi cela se produit - est-ce un bug, ou est-ce que j'utilise les coroutines dans le mauvais sens?
Mise à jour:
L'utilisation de coroutineScope { ... }
Atténuera le problème dans runFailingCoroutine()
fun runFailingCoroutine() = runBlocking {
try {
coroutineScope { async { fail() }.await() }
} catch (ex: Throwable) {
println("Catching ex in runFailingCoroutine(): $ex")
}
}
Le comportement de votre deuxième exemple est correct, c'est le travail de la concurrence structurée. Étant donné que le bloc async
interne lève une exception, cette coroutine est annulée. En raison de la simultanéité structurée, le travail parent est également annulé.
Regardez ce petit exemple:
val result = coroutineScope {
async {
throw IllegalStateException()
}
10
}
Ce bloc ne renverra jamais de valeur, même si nous ne demandons jamais le résultat async
. La coroutine intérieure est annulée et la portée extérieure est également annulée.
Si vous n'aimez pas ce comportement, vous pouvez utiliser le supervisorScope
. Dans ce cas, la coroutine intérieure peut échouer sans faire échouer la coroutine extérieure.
val result = supervisorScope {
async {
throw IllegalStateException()
}
10
}
Dans votre premier exemple, vous interceptez l'exception à l'intérieur du bloc de coroutine, à cause de cela, la coroutine se ferme normalement.
Pour une discussion sur ce sujet, voir:
J'ai été frappé par ce comportement hier, voici mon analyse .
En bref, ce comportement est souhaité car async
n'a pas le même objectif que dans d'autres langues. Dans Kotlin, vous devez l'utiliser avec parcimonie, uniquement lorsque vous devez décomposer une tâche en plusieurs sous-tâches qui s'exécutent en parallèle.
Chaque fois que vous voulez juste écrire
val result = async { work() }.await()
vous devriez plutôt écrire
val result = withContext(Default) { work() }
et cela se comportera comme prévu. De plus, chaque fois que vous en avez l'occasion, vous devez déplacer l'appel withContext
dans la fonction work()
et en faire un suspend fun
.