Ce code:
fun main() {
runBlocking {
try {
val deferred = async { throw Exception() }
deferred.await()
} catch (e: Exception) {
println("Caught $e")
}
}
println("Completed")
}
résultats dans cette sortie:
Caught Java.lang.Exception
Exception in thread "main" Java.lang.Exception
at org.mtopol.TestKt$main$1$deferred$1.invokeSuspend(test.kt:11)
...
Ce comportement n'a pas de sens pour moi. L'exception a été interceptée et gérée, et malgré tout, elle échappe au niveau supérieur en tant qu'exception non gérée.
Ce comportement est-il documenté et attendu? Cela viole toutes mes intuitions sur la manière dont la gestion des exceptions est censée fonctionner.
J'ai adapté cette question à partir d'un fil de discussion sur le forum Kotlin .
La documentation Kotlin suggère d'utiliser supervisorScope
si nous ne voulons pas annuler toutes les routines secondaires en cas d'échec. Donc je peux écrire
fun main() {
runBlocking {
supervisorScope {
try {
launch {
delay(1000)
println("Done after delay")
}
val job = launch {
throw Exception()
}
job.join()
} catch (e: Exception) {
println("Caught $e")
}
}
}
println("Completed")
}
La sortie est maintenant
Exception in thread "main" Java.lang.Exception
at org.mtopol.TestKt$main$2$1$job$1.invokeSuspend(test.kt:16)
...
at org.mtopol.TestKt.main(test.kt:8)
...
Done after delay
Completed
Encore une fois, ce n’est pas le comportement que je veux. Ici, une coroutine launch
ed a échoué avec une exception non gérée, invalidant le travail des autres routines, mais elles se poursuivent sans interruption.
Le comportement que je trouverais raisonnable est d’annuler l’annulation lorsqu’une coroutine échoue de manière imprévue (c’est-à-dire non gérée). Attraper une exception de await
signifie qu'il n'y a pas eu d'erreur globale, mais juste une exception localisée gérée dans le cadre de la logique métier.
Après avoir étudié les raisons pour lesquelles Kotlin a provoqué ce comportement, j’ai trouvé que, si les exceptions n’étaient pas propagées de cette façon, il serait compliqué d’écrire du code bien conçu qui sera annulé à temps. Par exemple:
runBlocking {
val deferredA = async {
Thread.sleep(10_000)
println("Done after delay")
1
}
val deferredB = async<Int> { throw Exception() }
println(deferredA.await() + deferredB.await())
}
Étant donné que a
est le premier résultat attendu, ce code continue à s'exécuter pendant 10 secondes, puis génère une erreur et ne produit aucun travail utile. Dans la plupart des cas, nous souhaitons tout annuler dès qu'un composant échoue. Nous pourrions le faire comme ceci:
val (a, b) = awaitAll(deferredA, deferredB)
println(a + b)
Ce code est moins élégant: nous sommes obligés d’attendre tous les résultats au même endroit et nous perdons la sécurité du type car awaitAll
renvoie une liste du sur-type commun de tous les arguments. Si nous avons des
suspend fun suspendFun(): Int {
delay(10_000)
return 2
}
et nous voulons écrire
val c = suspendFun()
val (a, b) = awaitAll(deferredA, deferredB)
println(a + b + c)
Nous sommes privés de la possibilité de renflouer avant que suspendFun
ne soit terminé. Nous pourrions travailler comme ceci:
val deferredC = async { suspendFun() }
val (a, b, c) = awaitAll(deferredA, deferredB, deferredC)
println(a + b + c)
mais ceci est fragile car vous devez faire attention à ce que vous le fassiez pour chaque appel pouvant être suspendu. C'est aussi contre la doctrine Kotlin du "séquentiel par défaut"
En conclusion: la conception actuelle, bien que paradoxale au premier abord, est logique en tant que solution pratique. De plus, cela renforce la règle de ne pas utiliser async-await
sauf si vous effectuez une décomposition parallèle d'une tâche.
Cela peut être résolu en modifiant légèrement le code pour que la valeur deferred
soit exécutée de manière explicite en utilisant la même CoroutineContext
que la portée runBlocking
, par exemple.
runBlocking {
try {
val deferred = withContext(this.coroutineContext) {
async {
throw Exception()
}
}
deferred.await()
} catch (e: Exception) {
println("Caught $e")
}
}
println("Completed")
MISE À JOUR APRÈS QUESTION ORIGINALE MISE À JOUR
Est-ce que cela fournit ce que vous voulez:
runBlocking {
supervisorScope {
try {
val a = async {
delay(1000)
println("Done after delay")
}
val b = async { throw Exception() }
awaitAll(a, b)
} catch (e: Exception) {
println("Caught $e")
// Optional next line, depending on whether you want the async with the delay in it to be cancelled.
coroutineContext.cancelChildren()
}
}
}
Ceci est tiré de this comment qui traite de la décomposition parallèle.
Bien que toutes les réponses soient exactes à cet endroit, mais laissez-moi vous éclairer un peu plus, ce qui pourrait aider d'autres utilisateurs. Il est documenté ici ( Doc officiel ) que: -
Si une coroutine rencontre une exception autre que
CancellationException
, il annule son parent à cette exception près. Ce comportement ne peut pas être substitué et est utilisé pour fournir des hiérarchies de coroutines stables pour concurrence simultanée structurée qui ne dépendent pas de CoroutineExceptionHandler implémentation. L'exception originale est géré par le parent (In GlobalScope) lorsque tous ses enfants se terminent.Cela n’a aucun sens d’installer un gestionnaire d’exceptions sur une coroutine qui est lancé dans le cadre du principal runBlocking , depuis le principal coroutine sera toujours annulé lorsque son enfant aura terminé avec une exception malgré le gestionnaire installé.
J'espère que cela aidera.
Une CoroutineScope
normale (créée par runBlocking
) annule immédiatement toutes les routines enfants lorsque l'un d'eux lève une exception. Ce comportement est documenté ici: https://kotlinlang.org/docs/reference/coroutines/exception-handling.html#cancellation-and-exceptions
Vous pouvez utiliser une supervisorScope
pour obtenir le comportement souhaité. Si une coroutine enfant échoue à l'intérieur d'un périmètre de supervision, les autres enfants ne seront pas immédiatement annulés. Les enfants ne seront annulés que si l'exception n'est pas gérée.
Pour plus d'informations, voir ici: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html
fun main() {
runBlocking {
supervisorScope {
try {
val deferred = async { throw Exception() }
deferred.await()
} catch (e: Exception) {
println("Caught $e")
}
}
}
println("Completed")
}