Sean McQuillan
Sean McQuillan

Follow

Apr 30, 2019 – 9 min lu

Ce billet fait partie d’une série en plusieurs parties sur l’utilisation des coroutines sur Android. Ce post se concentre sur le fonctionnement des coroutines et les problèmes qu’elles résolvent.

Autres articles de cette série:

Les coroutines Kotlin introduisent un nouveau style de concurrence qui peut être utilisé sur Android pour simplifier le code asynchrone. Bien qu’elles soient nouvelles pour Kotlin dans la version 1.3, le concept de coroutines existe depuis l’aube des langages de programmation. Le premier langage à explorer l’utilisation des coroutines était Simula en 1967.

Au cours des dernières années, les coroutines ont gagné en popularité et sont maintenant incluses dans de nombreux langages de programmation populaires tels que Javascript, C#, Python, Ruby et Go pour en nommer quelques-uns. Les coroutines de Kotlin sont basées sur des concepts établis qui ont été utilisés pour construire de grandes applications.

Sur Android, les coroutines sont une excellente solution à deux problèmes :

  1. Les tâches longues sont des tâches qui prennent trop de temps pour bloquer le thread principal.
  2. La sécurité principale vous permet de vous assurer que toute fonction de suspension peut être appelée depuis le thread principal.

Plongeons chacun d’entre eux pour voir comment les coroutines peuvent nous aider à structurer le code de manière plus propre !

Tâches à long terme

La récupération d’une page web ou l’interaction avec une API impliquent toutes deux de faire une requête réseau. De même, la lecture d’une base de données ou le chargement d’une image à partir du disque impliquent la lecture d’un fichier. Ces sortes de choses sont ce que j’appelle des tâches à long terme – des tâches qui prennent beaucoup trop de temps pour que votre application s’arrête et les attende !

Il peut être difficile de comprendre à quelle vitesse un téléphone moderne exécute du code par rapport à une requête réseau. Sur un Pixel 2, un seul cycle du processeur prend un peu moins de 0,0000000004 seconde, un nombre assez difficile à saisir en termes humains. Toutefois, si vous considérez qu’une requête réseau correspond à un clignement d’œil, soit environ 400 millisecondes (0,4 seconde), il est plus facile de comprendre la vitesse de fonctionnement du CPU. En un clignement d’œil, ou une demande réseau un peu lente, le CPU peut exécuter plus d’un milliard de cycles !

Sur Android, chaque application a un thread principal qui est chargé de gérer l’interface utilisateur (comme le dessin des vues) et de coordonner les interactions de l’utilisateur. Si trop de travail se passe sur ce thread, l’app semble se bloquer ou ralentir, ce qui conduit à une expérience utilisateur indésirable. Toute tâche de longue durée doit être effectuée sans bloquer le thread principal, afin que votre application n’affiche pas ce que l’on appelle le « jank », comme des animations figées, ou ne réponde pas lentement aux événements tactiles.

Pour effectuer une requête réseau hors du thread principal, un modèle commun est celui des callbacks. Les callbacks fournissent un handle à une bibliothèque qu’elle peut utiliser pour rappeler dans votre code à un moment ultérieur. Avec les callbacks, la récupération de developer.android.com pourrait ressembler à ceci:

class ViewModel: ViewModel() {
fun fetchDocs() {
get("developer.android.com") { result ->
show(result)
}
}
}

Même si get est appelé depuis le thread principal, il utilisera un autre thread pour effectuer la requête réseau. Ensuite, une fois que le résultat est disponible sur le réseau, le callback sera appelé sur le thread principal. C’est une excellente façon de gérer les tâches à long terme, et des bibliothèques comme Retrofit peuvent vous aider à faire des demandes de réseau sans bloquer le thread principal.

Utilisation des coroutines pour les tâches à long terme

Les coroutines sont un moyen de simplifier le code utilisé pour gérer les tâches à long terme comme fetchDocs. Pour explorer comment les coroutines rendent le code pour les tâches à longue exécution plus simple, réécrivons l’exemple de callback ci-dessus pour utiliser les coroutines.

// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}// look at this in the next section
suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}

Ce code ne bloque-t-il pas le thread principal ? Comment renvoie-t-il un résultat de get sans attendre la demande du réseau et sans bloquer ? Il s’avère que les coroutines fournissent un moyen pour Kotlin d’exécuter ce code et de ne jamais bloquer le thread principal.

Les coroutines s’appuient sur les fonctions régulières en ajoutant deux nouvelles opérations. En plus de invoke (ou call) et de return, les coroutines ajoutent suspend et resume.

  • suspend – met en pause l’exécution de la coroutine actuelle, en sauvegardant toutes les variables locales
  • resume – poursuit une coroutine suspendue à partir de l’endroit où elle a été mise en pause

Cette fonctionnalité est ajoutée par Kotlin par le mot-clé suspend sur la fonction. Vous ne pouvez appeler les fonctions suspendues qu’à partir d’autres fonctions suspendues, ou en utilisant un constructeur de coroutine comme launch pour démarrer une nouvelle coroutine.

Suspendre et reprendre fonctionnent ensemble pour remplacer les callbacks.

Dans l’exemple ci-dessus, get suspendra la coroutine avant de lancer la requête réseau. La fonction get sera toujours responsable de l’exécution de la requête réseau hors du thread principal. Ensuite, lorsque la requête réseau se termine, au lieu d’appeler une callback pour notifier le thread principal, elle peut simplement reprendre la coroutine qu’elle a suspendue.

Animation montrant comment Kotlin met en œuvre suspend et resume pour remplacer les callbacks.

En regardant comment fetchDocs s’exécute, vous pouvez voir comment suspend fonctionne. Chaque fois qu’une coroutine est suspendue, le cadre de pile actuel (l’endroit que Kotlin utilise pour garder la trace de la fonction en cours d’exécution et de ses variables) est copié et enregistré pour plus tard. Lorsqu’elle reprend, le cadre de la pile est copié à nouveau à partir de l’endroit où il a été sauvegardé et recommence à fonctionner. Au milieu de l’animation, lorsque toutes les coroutines du thread principal sont suspendues, le thread principal est libre de mettre à jour l’écran et de gérer les événements utilisateur. Ensemble, la suspension et la reprise remplacent les callbacks. Plutôt soigné !

Quand toutes les coroutines du thread principal sont suspendues, le thread principal est libre de faire d’autres travaux.

Même si nous avons écrit un code séquentiel simple qui ressemble exactement à une requête réseau bloquante, les coroutines exécuteront notre code exactement comme nous le voulons et éviteront de bloquer le thread principal !

Puis, regardons comment utiliser les coroutines pour la main-safety et explorons les dispatchers.

Main-safety avec les coroutines

Dans les coroutines Kotlin, les fonctions de suspension bien écrites sont toujours sûres à appeler depuis le thread principal. Peu importe ce qu’elles font, elles devraient toujours permettre à tout thread de les appeler.

Mais, il y a beaucoup de choses que nous faisons dans nos applications Android qui sont trop lentes pour se produire sur le thread principal. Les requêtes réseau, l’analyse de JSON, la lecture ou l’écriture de la base de données, ou même simplement l’itération sur de grandes listes. Chacun de ces éléments a le potentiel de s’exécuter assez lentement pour causer un « jank » visible par l’utilisateur et devrait s’exécuter hors du thread principal.

L’utilisation de suspend ne dit pas à Kotlin d’exécuter une fonction sur un thread d’arrière-plan. Il est utile de dire clairement et souvent que les coroutines s’exécuteront sur le thread principal. En fait, c’est une très bonne idée d’utiliser Dispatchers.Main.immediate lors du lancement d’une coroutine en réponse à un événement de l’interface utilisateur – de cette façon, si vous ne finissez pas par effectuer une tâche de longue durée qui nécessite une sécurité principale, le résultat peut être disponible dans la toute prochaine image pour l’utilisateur.

Les coroutines s’exécuteront sur le thread principal, et suspendre ne veut pas dire arrière-plan.

Pour rendre une fonction qui effectue un travail trop lent pour le thread principal main-safe, vous pouvez dire aux coroutines Kotlin d’effectuer le travail sur le dispatcher Default ou IO. En Kotlin, toutes les coroutines doivent s’exécuter dans un répartiteur – même lorsqu’elles s’exécutent sur le thread principal. Les coroutines peuvent se suspendre elles-mêmes, et le répartiteur est la chose qui sait comment les reprendre.

Pour spécifier où les coroutines doivent s’exécuter, Kotlin fournit trois répartiteurs que vous pouvez utiliser pour la répartition des threads.

+-----------------------------------+
| Dispatchers.Main |
+-----------------------------------+
| Main thread on Android, interact |
| with the UI and perform light |
| work |
+-----------------------------------+
| - Calling suspend functions |
| - Call UI functions |
| - Updating LiveData |
+-----------------------------------+
+-----------------------------------+
| Dispatchers.IO |
+-----------------------------------+
| Optimized for disk and network IO |
| off the main thread |
+-----------------------------------+
| - Database* |
| - Reading/writing files |
| - Networking** |
+-----------------------------------+
+-----------------------------------+
| Dispatchers.Default |
+-----------------------------------+
| Optimized for CPU intensive work |
| off the main thread |
+-----------------------------------+
| - Sorting a list |
| - Parsing JSON |
| - DiffUtils |
+-----------------------------------+

* Room fournira automatiquement la sécurité principale si vous utilisez des fonctions de suspension, RxJava, ou LiveData.

** Les bibliothèques de mise en réseau telles que Retrofit et Volley gèrent leurs propres threads et ne nécessitent pas de main-safety explicite dans votre code lorsqu’elles sont utilisées avec les coroutines Kotlin.

Pour continuer avec l’exemple ci-dessus, utilisons les dispatchers pour définir la fonction get. À l’intérieur du corps de get, vous appelez withContext(Dispatchers.IO) pour créer un bloc qui sera exécuté sur le dispatcher IO. Tout code que vous mettez à l’intérieur de ce bloc sera toujours exécuté sur le répartiteur IO. Puisque withContext est lui-même une fonction de suspension, il fonctionnera en utilisant les coroutines pour fournir une sécurité principale.

// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}// Dispatchers.Main
suspend fun get(url: String) =
// Dispatchers.Main
withContext(Dispatchers.IO) {
// Dispatchers.IO
/* perform blocking network IO here */
}
// Dispatchers.Main

Avec les coroutines, vous pouvez faire la répartition des threads avec un contrôle à grain fin. Parce que withContext vous permet de contrôler sur quel thread toute ligne de code s’exécute sans introduire un callback pour retourner le résultat, vous pouvez l’appliquer à de très petites fonctions comme la lecture de votre base de données ou l’exécution d’une requête réseau. Ainsi, une bonne pratique consiste à utiliser withContext pour s’assurer que chaque fonction est sûre d’être appelée sur n’importe quel Dispatcher, y compris Main – de cette façon, l’appelant n’a jamais à penser à quel thread sera nécessaire pour exécuter la fonction.

Dans cet exemple, fetchDocs s’exécute sur le thread principal, mais peut appeler en toute sécurité get qui effectue une requête réseau en arrière-plan. Parce que les coroutines supportent la suspension et la reprise, la coroutine sur le thread principal sera reprise avec le résultat dès que le bloc withContext sera terminé.

Les fonctions de suspension bien écrites sont toujours sûres d’être appelées depuis le thread principal (ou main-safe).

C’est une très bonne idée de rendre chaque fonction de suspension main-safe. Si elle fait quelque chose qui touche le disque, le réseau, ou même simplement utilise trop de CPU, utilisez withContext pour la rendre sûre à appeler depuis le thread principal. C’est le modèle que suivent les bibliothèques basées sur les coroutines comme Retrofit et Room. Si vous suivez ce style dans l’ensemble de votre base de code, votre code sera beaucoup plus simple et évitera de mélanger les préoccupations de threading avec la logique de l’application. Lorsqu’elles sont suivies de manière cohérente, les coroutines sont libres de se lancer sur le thread principal et de faire des demandes de réseau ou de base de données avec un code simple tout en garantissant que les utilisateurs ne verront pas de « jank ». »

Les performances de withContext

withContext sont aussi rapides que celles des callbacks ou de RxJava pour fournir une sécurité principale. Il est même possible d’optimiser les appels withContext au-delà de ce qui est possible avec les callbacks dans certaines situations. Si une fonction doit faire 10 appels à une base de données, vous pouvez dire à Kotlin de passer une fois dans un withContext extérieur autour des 10 appels. Ensuite, même si la bibliothèque de base de données appelle withContext à plusieurs reprises, elle restera sur le même dispatcher et suivra un chemin rapide. En outre, la commutation entre Dispatchers.Default et Dispatchers.IO est optimisée pour éviter les commutations de threads lorsque cela est possible.

Quoi de neuf

Dans ce post, nous avons exploré quels problèmes les coroutines sont excellentes pour résoudre. Les coroutines sont un concept vraiment ancien dans les langages de programmation qui sont devenus populaires récemment en raison de leur capacité à rendre le code qui interagit avec le réseau plus simple.

Sur Android, vous pouvez les utiliser pour résoudre deux problèmes vraiment communs :

  1. Simplifier le code pour les tâches à long terme telles que la lecture du réseau, du disque, ou même l’analyse d’un grand résultat JSON.
  2. Préparer une main-safety précise pour s’assurer que vous ne bloquez jamais accidentellement le thread principal sans rendre le code difficile à lire et à écrire.

Dans le prochain post, nous explorerons comment ils s’intègrent sur Android pour garder la trace de tout le travail que vous avez commencé depuis un écran ! Donnez-lui une lecture:

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.