Kotlin
A concise multiplatform language developed by JetBrains
Kotlin Coroutines 1.5 : GlobalScope signalée comme API “sensible”, amélioration de l’API de canaux, et plus encore
La version 1.5.0 de Kotlin Coroutines est là ! Voici ce que cette nouvelle version apporte :
- GlobalScope est maintenant signalée comme API “sensible”, nécessitant une attention particulière. Elle offre en effet des fonctionnalités avancées qui peuvent facilement donner lieu à une utilisation incorrecte. Dorénavant, le compilateur vous avertit en cas de risque d’utilisation incorrecte et un opt-in sera requis pour l’utilisation de cette classe dans votre programme.
- Extensions pour JUnit. CoroutinesTimeout est maintenant disponible pour JUnit 5.
- Amélioration de l’API de canaux. En plus des nouvelles règles de nommage pour les fonctions de la bibliothèque, les fonctions non suspensives
trySend
ettryReceive
ont été introduites comme de meilleures alternatives àoffer
etpoll
.
- Stabilisation des intégrations Reactive. Nous avons ajouté plus de fonctions pour la conversion des types Reactive Streams en Kotlin Flow et inversement, et stabilisé de nombreuses fonctions existantes et l’API ReactiveContext.
Vous trouverez toutes les recommandations pour passer à la nouvelle version en fin d’article.
Commencer à utiliser Coroutines 1.5.0
GlobalScope signalée comme API à appréhender avec précaution
La classe GlobalScope
est désormais marquée avec l’annotation @DelicateCoroutinesApi
. Dorénavant, toute utilisation de GlobalScope
nécessitera un opt-in explicite avec @OptIn(DelicateCoroutinesApi::class)
.
Bien que l’utilisation de GlobalScope
ne soit pas recommandée dans la plupart des cas, la documentation officielle propose un certain nombre de concepts utilisant cette API.
Un CoroutineScope
global n’est lié à aucune tâche. Global scope est utilisée pour exécuter des coroutines de niveau supérieur qui fonctionnent pendant toute la durée de vie de l’application et ne sont pas annulées prématurément. Les coroutines actives lancées dans GlobalScope
ne permettent pas d’éviter l’arrêt du processus. Elles sont similaires à des threads démons.
L’utilisation de l’API GlobalScope
est délicate et requiert de la prudence car elle peut facilement engendrer des pertes de ressources ou de mémoire. Une coroutine lancée dans GlobalScope
n’obéit pas au principe de concurrence structurée, donc en cas de blocage ou de ralentissement (en raison de la lenteur du réseau par exemple), elle continuera de fonctionner et de consommer des ressources. Voici un exemple :
L’appel à loadConfiguration
crée une coroutine dans GlobalScope
qui s’exécute en arrière-plan et aucune condition n’est spécifiée pour l’annuler ou attendre son achèvement. Si le réseau est lent, elle reste en attente en arrière-plan et consomme des ressources. Des appels répétés à loadConfiguration
entraîneront la consommation de plus en plus de ressources.
Possibilités de remplacement
Dans de nombreux cas, l’utilisation de GlobalScope
doit être évitée et l’opération contenante doit être marquée avec suspend
, par exemple :
Dans les cas où GlobalScope.launch
est utilisé pour lancer plusieurs opérations simultanées, les opérations correspondantes doivent plutôt être regroupées avec coroutineScope
:
Dans le code de niveau supérieur, lors du lancement d’une opération concurrente simultanée à partir d’un contexte non suspensif, utilisez une instance CoroutineScope
correctement délimitée au lieu de GlobalScope
.
Cas d’utilisation appropriés
L’utilisation de GlobalScope
est sûre et justifiée dans quelques cas, parmi lesquels les processus d’arrière-plan de niveau supérieur qui doivent s’exécuter pendant toute la durée de vie d’une application. C’est pourquoi toute utilisation de GlobalScope
requiert un opt-in explicite avec @OptIn(DelicateCoroutinesApi::class)
:
Nous vous recommandons de passer en revue toutes vos utilisations de GlobalScope
et d’annoter uniquement celles qui rentrent dans la catégorie des « cas d’utilisation appropriés ». Pour tous les autres cas, pour éviter les bugs dans votre code, il faut remplacer l’utilisation de GlobalScope
comme décrit ci-dessus.
Extensions pour JUnit 5
Nous avons ajouté une annotation CoroutinesTimeout
qui vous permet d’exécuter des tests dans un thread séparé, de les désactiver une fois le temps imparti expiré et d’interrompre le thread. Auparavant, CoroutinesTimeout
était seulement disponible pour JUnit 4. Avec cette version, nous avons ajouté l’intégration pour JUnit 5.
Pour utiliser la nouvelle annotation, ajoutez la dépendance suivante à votre projet :
Voici un exemple simple de l’utilisation de CoroutinesTimeout
dans vos tests :
Dans cet exemple, le délai d’expiration des coroutines est défini au niveau de la classe et spécifiquement pour firstTest
. Il n’y a pas de délai imparti pour le test annoté car l’annotation de la fonction prévaut sur celle de la classe. En revanche il y en a un pour secondTest
, qui utilise l’annotation au niveau de la classe.
L’annotation est déclarée de la façon suivante :
Le premier paramètre, testTimeoutMs
, spécifie la durée du délai imparti en millisecondes. Le deuxième paramètre, cancelOnTimeout
, détermine si toutes les coroutines en cours d’exécution doivent être annulées à la fin du délai imparti. S’il est défini comme true
, toutes les coroutines seront automatiquement annulées.
Lorsque vous utilisez l’annotation CoroutinesTimeout
, elle active automatiquement le débogueur de coroutines et crée un dump de toutes les coroutines lorsque le délai imparti a expiré. Le dump contient les traces de pile de la création des coroutines. Si vous devez désactiver les traces de pile afin d’accélérer les tests vous pouvez utiliser CoroutinesTimeoutExtension
directement pour définir les paramètres appropriés.
Un grand merci à Abhijit Sarkar pour son PoC particulièrement utile pour CoroutinesTimeout pour JUnit 5. L’idée a été développée dans la nouvelle annotation CoroutinesTimeout
que nous avons ajoutée dans la version 1.5.
Amélioration de l’API de canaux
Les canaux constituent des primitives de communication importantes, qui permettent l’échange de données entre coroutines et callbacks. Pour cette version, nous avons retravaillé l’API de canaux, en remplaçant les fonctions offer
et poll
, qui prêtaient à confusion, par de meilleures alternatives. Au passage, nous avons développé un nouveau système de nommage cohérent pour les méthodes suspensives et non suspensives.
Nouveau schéma de nommage
Nous avons voulu créer des règles de nommage cohérentes qui pourraient ensuite être utilisées dans d’autres bibliothèques ou API de coroutine. Nous devions nous assurer que le nom de la fonction transmettrait les informations sur son comportement. Voici ce à quoi nous sommes parvenus :
- Les méthodes suspensives régulières sont laissées telles quelles, par exemple
send
etreceive
. - Tous les noms des méthodes non suspensives avec encapsulation d’erreur sont systématiquement préfixés par « try » :
trySend
ettryReceive
au lieu deoffer
etpoll
. - Les nouvelles méthodes suspensives d’encapsulation d’erreur auront le suffixe « Catching ».
Voyons ces nouvelles méthodes plus en détails.
Fonctions Try
: équivalents non suspensifs de send
et receive
Une coroutine peut envoyer des informations à un canal, tandis que l’autre peut recevoir ces informations de ce canal. Les fonctions send
et receive
sont toutes les deux suspensives. send
suspend sa coroutine si le canal est plein et ne peut pas prendre en compte de nouvel élément et receive
suspend sa coroutine si le canal n’a aucun élément à retourner :
Ces fonctions ont des équivalents non suspensifs pour une utilisation dans du code synchrone : offer
et poll
, qui sont remplacés par trySend
et tryReceive
, et la prise en charge de l’ancienne fonctionnalité est interrompue. Voyons quelles sont les raisons de ce changement.
Les fonctions offer
et poll
sont censées avoir le même comportement que send
et receive
, mais sans suspension. Cela semble simple et c’est le cas tant que l’élément peut être envoyé ou reçu. Mais qu’arriverait-t-il en cas d’erreur ? send
et receive
seraient alors suspendues jusqu’à ce qu’elles puissent de nouveau fonctionner correctement. offer
et poll
retournaient simplement false
et null
respectivement si l’élément n’avait pas pu être ajouté parce que le canal était plein ou si aucun élément n’avait pu être récupéré car le canal était vide. Elles ont tous les deux lancé une exception pour tenter de travailler avec un canal fermé, ce qui a causé des problèmes avec leur utilisation.
Dans cet exemple, poll
est appelé avant qu’un élément ne soit ajouté et renvoie donc null
immédiatement. Notez que cette fonction n’est pas censée être utilisée de cette façon : il est préférable de continuer à interroger les éléments régulièrement. Nous l’appelons directement pour simplifier cette explication. L’appel de offer
est également infructueux car il s’agit d’un canal rendezvous ayant une capacité de mémoire tampon nulle. Par conséquent, offer
renvoie false
et poll
renvoie null
, simplement parce qu’elles n’ont pas été appelées dans bon ordre.
Dans l’exemple ci-dessus, essayez de décommenter l’instruction channel.close()
pour vous assurer que l’exception est levée. Dans ce cas, poll
renvoie false
, comme précédemment. Mais ensuite offer
essaie d’ajouter un élément à un canal déjà fermé, échoue et lance une exception. Nous avons reçu de nombreuses remarques selon lesquelles ce comportement est source d’erreurs. Il est facile d’oublier de gérer cette exception, or l’ignorer ou la traiter différemment aura pour conséquence de planter votre programme.
Les nouvelles fonctions trySend
et tryReceive
corrigent ce problème et renvoient un résultat plus détaillé. Chacune renvoie l’instance ChannelResult
, qui peut indiquer trois choses : un résultat positif, un échec ou la fermeture du canal.
Cet exemple fonctionne de la même manière que le précédent. La seule différence est que tryReceive
et trySend
renvoient un résultat plus détaillé. Vous pouvez voir le résultat Value(Failed)
au lieu de false
et null
. Décommentez la ligne fermant à nouveau le canal et assurez-vous que trySend
renvoie maintenant un résultat Closed
capturant une exception.
Grâce aux classes de valeurs inline, l’utilisation de ChannelResult
ne crée pas de wrappers supplémentaires en dessous et si la valeur réussie est renvoyée, elle l’est telle quelle, sans surcharge.
Fonctions Catching : suspendre les fonctions qui encapsulent des erreurs
À partir de cette version, les méthodes suspensives d’encapsulation des erreurs auront le suffixe « Catching ». Par exemple, la nouvelle fonction receiveCatching
gère l’exception dans le cas d’un canal fermé. Prenons cet exemple simple :
Le canal est fermé avant que nous essayions de récupérer une valeur. Cependant, le programme aboutit avec succès, indiquant que le canal a été fermé. Si vous remplacez receiveCatching
par la fonction receive
ordinaire, elle lancera ClosedReceiveChannelException
:
Actuellement, nous fournissons seulement receiveCatching
et onReceiveCatching
(au lieu de la fonction interne receiveOrClosed
auparavant), mais nous prévoyons d’ajouter d’autres fonctions.
Migration de votre code vers de nouvelles fonctions
Vous pouvez remplacer automatiquement toutes les utilisations des fonctions offer
et poll
dans votre projet avec de nouveaux appels. Puisque offer
a renvoyé Boolean
, son équivalent de remplacement est canal.trySend("Element").isSuccess
.
De même, la fonction poll
renvoie un élément nullable, son remplacement devient donc canal.tryReceive().getOrNull()
.
Si le résultat de l’appel n’a pas été utilisé, vous pouvez les remplacer directement par de nouveaux appels.
Le comportement de traitement des exceptions est désormais différent, vous devrez donc effectuer les mises à jour nécessaires manuellement. Si votre code repose sur les méthodes « offer » et « poll » qui lancent des exceptions sur un canal fermé, vous devrez utiliser les remplacements suivants.
Le remplacement équivalent pour canal.offer("Element")
devrait lever une exception lorsque le canal est fermé, même s’il a été fermé normalement :
Le remplacement équivalent pour channel.poll()
lance une exception si le canal a été fermé avec une erreur et renvoie null
s’il a été fermé normalement :
Ces changements correspondent à l’ancien comportement des fonctions offer
et poll
.
Nous sommes partis du postulat que, dans la plupart des cas, votre code ne reposait pas sur ces subtilités de comportement sur un canal fermé, mais que cela était plutôt une source de bugs. C’est pourquoi les remplacements automatiques fournis par l’IDE simplifient la sémantique. Si cela ne correspond pas à votre cas, veuillez passer en revue vos utilisations et les mettre à jour manuellement et envisager de les réécrire complètement pour traiter les cas de canaux fermés différemment, sans lever d’exceptions.
Les intégrations Reactive sur la voie de la stabilité
Avec la version 1.5 de Kotlin Coroutines, la plupart des fonctions responsables des intégrations avec les frameworks reactive sont maintenant stables.
Dans l’écosystème de la JVM, quelques frameworks traitent la gestion des threads asynchrones selon la norme des Reactive Streams. Les frameworks Java Project Reactor et RxJava sont sont de ceux là.
Bien que les Kotlin Flows soient différents et que les types ne soient pas compatibles avec ceux spécifiés par la norme, ce sont néanmoins des flux. Il est possible de convertir Flow
en Reactive (en conformité avec les spécifications et TCK) Publisher
et vice versa. Ces convertisseurs sont directement fournis par kotlinx.coroutines
et peuvent être trouvés dans les modules Reactive correspondants.
Par exemple, si vous avez besoin d’interopérabilité avec les types de Project Reactor, vous devez ajouter les dépendances suivantes à votre projet :
Vous pourrez alors utiliser Flow<T>.asPublisher()
si vous voulez utiliser les types Reactive Streams ou Flow<T>.asFlux()
si vous devez utiliser directement les types Project Reactor.
Pour en savoir plus sur les Reactive Streams et les Kotlin Flows, consultez l’article de Roman Elizarov.
Bien que les intégrations avec les bibliothèques Reactive contribuent à la stabilisation de l’API, d’un point de vue technique, l’objectif est de se débarrasser des @ExperimentalCoroutinesApi
et de corriger les bugs.
Meilleure intégration avec Reactive Streams<
La compatibilité avec les spécifications de Reactive Streams est importante afin d’assurer l’interopérabilité entre les frameworks tiers et les coroutines Kotlin. Cela permet d’adopter les coroutines Kotlin dans les projets hérités sans avoir à réécrire tout le code.
Nous sommes parvenus à faire évoluer et à stabiliser de nombreuses fonctions. Il est maintenant possible de convertir un type de n’importe quelle implémentation Reactive Streams en Flow
et inversement. Par exemple, le nouveau code peut être écrit avec des coroutines, mais intégré à l’ancienne base de code Reactive via les convertisseurs opposés :
Nous avons également apporté de nombreuses améliorations àReactorContext
, qui encapsule le Context de Reactor dans CoroutineContext, pour une intégration fluide et complète entre Project Reactor et Kotlin Coroutines. Grâce à cette intégration, il est possible de propager les informations du Context de Reactor à travers des coroutines.
Le contexte est implicitement propagé via le contexte des subscribers par toutes les intégrations Reactive, telles que Mono
, Flux
, Publisher.asFlow
, Flow.asPublisher
et Flow.asFlux
. Voici un exemple simple de distribution du context
du subscriber dans ReactorContext
:
Dans l’exemple ci-dessus, nous construisons une instance Flow
qui est ensuite convertie en instance Flux de Reactor, sans contexte. Appeler la méthode subscribe()
sans argument a pour conséquence de demander au publisher d’envoyer toutes les données. En conséquence, le programme affiche la phrase « Reactor context in Flow: null ».
De même, la chaîne d’appels suivante convertit Flow
en Flux
, mais ajoute ensuite une paire clé-valeur réponse=42 au contexte Reactor pour cette chaîne. L’appel à subscribe()
déclenche la chaîne. Dans ce cas, puisque le contexte est renseigné, le programme affiche « Reactor context in Flow: Context1{answer=42} ».
Nouvelles fonctions d’assistance
Lorsque vous utilisez des types réactifs comme Mono
dans le contexte des coroutines, des fonctions d’aide vous permettent de récupérer les données sans bloquer le thread. Avec cette version, nous arrêtons la prise en charge des fonctions awaitSingleOr*
dans les Publisher
arbitraires et avons spécialisé certaines fonctions await*
pour Mono
et Maybe
.
Mono
produit au plus une valeur, le dernier élément est donc le même que le premier. Dans ce cas, la sémantique de suppression des éléments restants est également inutile. Par conséquent, la prise en charge de Mono.awaitFirst()
et Mono.awaitLast()
est interrompue et remplacée par celle de Mono.awaitSingle()
.
Commencez à utiliser kotlinx.coroutines 1.5.0 !
Cette nouvelle version apporte un nombre impressionnant de nouveautés. Le nouveau schéma de nommage mis au point lors du perfectionnement de l’API de canaux est l’une des réalisations les plus remarquable de l’équipe. Parallèlement, nous nous efforçons de rendre l’API des coroutines aussi simple et intuitive que possible.
Pour commencer à utiliser la nouvelle version de Kotlin Coroutines, il suffit de mettre à jour le contenu de votre fichier build.gradle.kts. Assurez-vous d’abord de disposer de la dernière version du plugin Kotlin Gradle :
Puis, mettez à jour les versions des dépendances, y compris les bibliothèques avec des intégrations spécifiques pour les Reactive Streams.
Plus de vidéos et d’articles
- Vidéo sur Kotlin Coroutines 1.5.0
- Guide des coroutines
- Documentation de l’API
- Dépôt GitHub de Kotlin Coroutines
- Article de blog sur Coroutines 1.4.0
- Article de blog sur Kotlin 1.5.0
En cas de problème
- Signalez-nous tout problème sur GitHub.
- Vous pouvez également demander de l’aide dans le canal Slack #coroutines de Kotlin (obtenir une invitation).
Auteurs de l’article original en anglais : Anton Arhipov et Svetlana Isakova