Tutoriel d'arrière-plan Android. Partie 5: Coroutines à Kotlin


Île Kotlin

Textes précédents de cette série: sur AsyncTask , sur Loaders , sur Executors et EventBus , sur RxJava .

Cette heure est donc venue. C'est l'article pour lequel toute la série a été écrite: une explication du fonctionnement de la nouvelle approche «sous le capot». Si vous ne savez pas encore comment l’utiliser, voici quelques liens utiles pour vous aider à démarrer:


Et après avoir maîtrisé les coroutines, vous pouvez vous demander ce qui a permis à Kotlin de fournir cette opportunité et comment cela fonctionne. Veuillez noter qu'ici, nous nous concentrerons uniquement sur l'étape de compilation: vous pouvez écrire un article séparé sur l'exécution.

La première chose que nous devons comprendre, c'est que dans le corpus, il n'existe en fait aucune coroutine. Le compilateur transforme la fonction avec le modificateur de suspension en fonction avec le paramètre Continuation . Cette interface a deux méthodes:

abstract fun resume(value: T) abstract fun resumeWithException(exception: Throwable) 

Le type T est le type de retour de votre fonction de suspension d'origine. Et voici ce qui se passe réellement: cette fonction est exécutée dans un certain thread (patience, nous y arrivons également), et le résultat est passé à la fonction de reprise de cette suite, dans le contexte de laquelle la fonction de suspension a été appelée. Si la fonction ne reçoit pas le résultat et lève une exception, resumeWithException est levée, lançant une erreur au code appelant.

D'accord, mais d'où vient la suite? Bien sûr, du constructeur Corutin! Regardons le code qui crée une coroutine, par exemple, lancez:

 public actual fun launch( context: CoroutineContext = DefaultDispatcher, start: CoroutineStart = CoroutineStart.DEFAULT, parent: Job? = null, block: suspend CoroutineScope.() -> Unit ): Job { val newContext = newCoroutineContext(context, parent) val coroutine = if (start.isLazy) LazyStandaloneCoroutine(newContext, block) else StandaloneCoroutine(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine } 

Ici, le générateur crée une coroutine - une instance de la classe AbstractCoroutine, qui, à son tour, implémente l'interface Continuation. La méthode de démarrage appartient à l'interface Job. Mais trouver la définition de la méthode de démarrage est très difficile. Mais nous pouvons venir ici de l'autre côté. Un lecteur attentif a déjà remarqué que le premier argument de la fonction de lancement est le CoroutineContext, et il est défini sur DefaultDispatcher par défaut. Les répartiteurs sont des classes qui contrôlent l'exécution des coroutines, ils sont donc très importants pour comprendre ce qui se passe. Regardons la déclaration DefaultDispatcher:

 public actual val DefaultDispatcher: CoroutineDispatcher = CommonPool 

Donc, en fait, c'est CommonPool, bien que les docks java nous disent que cela peut changer. Qu'est-ce que CommonPool?

Il s'agit d'un gestionnaire de coroutine utilisant ForkJoinPool comme implémentation d'ExecutorService. Oui, c'est le cas: au final, toutes vos coroutines lambda ne sont que Runnable, piégées dans Executor avec un tas de transformations délicates. Mais le diable, comme toujours, est dans les détails.


Fourchette? Ou rejoindre?

À en juger par les résultats de l'enquête sur mon twitter, je dois ici expliquer brièvement ce qu'est le FJP :)


Tout d'abord, ForkJoinPool est un exécuteur moderne créé pour être utilisé avec les flux parallèles Java 8. La tâche d'origine était un parallélisme efficace lors de l'utilisation de l'API Stream, ce qui signifie essentiellement de diviser les flux pour traiter une partie des données, puis de les combiner lorsque toutes les données ont été traitées. Pour simplifier, imaginez que vous disposez du code suivant:

 IntStream .range(1, 1_000_000) .parallel() .sum() 

La quantité d'un tel flux ne sera pas calculée dans un flux, à la place, ForkJoinPool divisera récursivement la plage en parties (d'abord en deux parties de 500 000, puis chacune en 250 000, etc.), calculera la somme de chaque partie et combinera les résultats en un seul montant. Voici une visualisation d'un tel processus:


Les threads sont divisés pour différentes tâches et fusionnés à nouveau après l'achèvement

L'efficacité de FJP est basée sur l'algorithme de «vol de travail»: lorsqu'un thread particulier manque de tâches, il va dans les files d'attente d'autres threads de pool et vole leurs tâches. Pour une meilleure compréhension, vous pouvez voir le rapport d' Alexei Shipilev ou regarder une présentation .

Eh bien, nous avons réalisé ce que font nos coroutines! Mais comment finissent-ils là-bas?

Cela se produit dans la méthode de répartition CommonPool #:

 _pool.execute(timeSource.trackTask(block)) 

La méthode d'expédition est appelée à partir de la méthode resume (Value: T) dans DispatchedContinuation. Cela semble familier! Nous nous souvenons que Continuation est une interface implémentée dans AbstractCoroutine. Mais comment sont-ils liés?

L'astuce est à l'intérieur de la classe CoroutineDispatcher. Il implémente l'interface ContinuationInterceptor comme suit:

 public actual override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> = DispatchedContinuation(this, continuation) 

Tu vois? Vous fournissez un bloc simple à la corutine du générateur. Vous n'avez pas besoin d'implémenter d'interfaces dont vous ne voulez rien savoir. La bibliothèque coroutine s'occupe de tout cela. Elle est
intercepte l'exécution, remplace la continuation par DispatchedContinuation et l'envoie à l'exécuteur, ce qui garantit l'exécution la plus efficace de votre code.

Maintenant, la seule chose dont nous avons besoin est de savoir comment la répartition est appelée à partir de la méthode start. Comblons cette lacune. La méthode de reprise est appelée depuis startCoroutine dans la fonction d'extension du bloc:

 public fun <R, T> (suspend R.() -> T).startCoroutine( receiver: R, completion: Continuation<T> ) { createCoroutineUnchecked(receiver, completion).resume(Unit) } 

Et startCoroutine, à son tour, est appelé par l'opérateur "()" dans l'énumération CoroutineStart. Votre générateur l'accepte comme deuxième paramètre, et la valeur par défaut est CoroutineStart.DEFAULT. C'est tout!

C'est la raison pour laquelle j'admire l'approche de la corutine: ce n'est pas seulement une syntaxe spectaculaire, mais aussi une implémentation brillante.

Et pour ceux qui ont lu jusqu'au bout, ils obtiennent en exclusivité: une vidéo de mon reportage «Le violoniste n'est pas nécessaire: on refuse RxJava en faveur de la coroutine à Kotlin» de la conférence Mobius . Profitez :)

Source: https://habr.com/ru/post/fr415335/


All Articles