Coroutines de bricolage. Partie 1. Générateurs paresseux

Dans le monde JVM, les coroutines sont mieux connues grâce au langage Kotlin et Project Loom . Je n'ai pas vu une bonne description du principe des coroutines Kotlinovsky, et le code de la bibliothèque kotlin-coroutines est complètement incompréhensible pour une personne non préparée. D'après mon expérience, la plupart des gens ne connaissent des coroutines que ce sont des "flux légers" et que dans kotlin, ils fonctionnent grâce à la génération de bytecode intelligent. J'étais donc jusqu'à récemment. Et l'idée m'est venue que puisque les coroutines peuvent être implémentées en bytecode, pourquoi ne pas les implémenter en java. De cette idée, par la suite, une petite bibliothèque assez simple est apparue, dont l'appareil, je l'espère, peut être compris par presque tous les développeurs. Détails sous la coupe.



Code source


J'ai appelé le projet Microutines, qui venait des mots Micro et Coroutines.


Tout le code est disponible sur github . Au départ, je voulais construire le récit en fonction du développement de ma propre compréhension du sujet, parler de mes mauvaises décisions et idées, du développement de l'API, mais ensuite le code de l'article serait très différent du code avec github (ce qui est inévitable dans tous les cas, j'écris un article fois, et sur github comique de temps en temps). Par conséquent, je décrirai principalement la version finale de la bibliothèque, et si certaines parties de l'API ne sont pas très claires à première vue, elles sont probablement nécessaires pour résoudre les problèmes que nous examinerons dans les articles suivants.


Clause de non-responsabilité


Il s'agit d'une sorte de projet de formation. Je serai heureux si l'un des lecteurs l'incline à jouer ou même à lancer une demande de tirage. Mais l'utiliser en production n'en vaut pas la peine. La meilleure façon de comprendre une technologie en profondeur est de la mettre en œuvre vous-même, ce qui est le seul objectif de ce projet.


Et je ne garantis pas l'exactitude des termes utilisés. Peut-être certains d'entre eux que j'ai entendus quelque part, mal mémorisés, et certains d'entre eux sont même venus avec moi et ont oublié.


Que sont les coroutines


Comme je l'ai déjà remarqué, on dit souvent à propos des coroutines qu'il s'agit de «flux facilités». Ce n'est pas une vraie définition. Je ne donnerai pas non plus une vraie définition, mais j'essaierai de décrire ce qu'elles sont, des coroutines. L'appel de flux coroutines ne sera pas entièrement correct. Coroutine est une unité de planification plus petite qu'un flux, et un flux, à son tour, est plus petit qu'une unité de planification. La planification des processus et des threads est gérée par le système d'exploitation. Corutin est engagé dans la planification ... nous serons nous-mêmes engagés dans leur planification. Les coroutines fonctionnent au-dessus des threads normaux, et leur principal truc est qu'elles ne bloquent pas le thread lorsqu'elles attendent qu'une autre tâche soit terminée, mais la libèrent pour une autre coroutine. Cette approche est appelée multitâche coopératif. Corutin peut fonctionner d'abord dans un fil, puis dans un autre. Un thread pour coroutine agit comme une ressource, et un million de coroutines peuvent fonctionner sur un seul thread. Vous pouvez voir cette photo:



Les tâches des contributions 1 et des contributions 2 répondent à une sorte de demande et ne bloquent pas le flux en attendant les réponses, mais suspendent leur travail et le poursuivent après avoir reçu les réponses. Nous pouvons écrire un tel code à l'aide de rappels, dites-vous. C'est vrai, mais l'essence de coroutine est que nous écrivons du code sans rappel, nous écrivons du code séquentiel ordinaire qui s'exécute de manière asynchrone.


Générateurs


Nous allons commencer le développement de simple à complexe. La première chose que nous ferons est la génération de collection paresseuse, implémentée dans certaines langues à l'aide du mot-clé yield. Les générateurs ne sont pas accidentels ici, comme nous le verrons plus tard, et les générateurs et coroutines peuvent être implémentés en utilisant le même mécanisme.


Prenons un exemple en python, simplement parce que les générateurs sont prêts à l'emploi.


def generator(): k = 10 yield k k += 10 yield k k += 10 yield k for i in generator(): print(i) 

Le cycle se déroule en quelque chose comme ça (peut-être pas tout à fait comme ça, mais le principe est important pour nous):


 gen = generator() while True: try: i = next(gen) print(i) except StopIteration: break 

Un appel à generator() créera un itérateur spécial appelé générateur. Le premier appel à next(gen) exécute le code depuis le début de la fonction generator jusqu'au premier yield , et la valeur de la variable locale k de genertator() écrite dans la variable i . Chaque prochain appel au next continuera à exécuter la fonction avec l'instruction suivant immédiatement le yield précédent yield et ainsi de suite. Dans ce cas, entre les next appels, les valeurs de toutes les variables locales à l'intérieur du generator sont enregistrées.


C'est à peu près la même chose, mais dans la langue Kotlin.


 val seq = sequence { var i = 10 yield(i) i += 10 yield(i) i += 10 yield(i) } for (i in seq) { println(i) } 

En Java, nous pourrions faire quelque chose comme la génération paresseuse:


 Iterable<Integer> seq = DummySequence.first(() -> { final int i = 10; return DummySequence.next(i, () -> { final int i1 = i + 10; return DummySequence.next(i1, () -> DummySequence.end(i1 + 10)); }); }); for(int i: seq) { System.out.println(i); } 

Implémentation de DummySequence
 import org.junit.Assert; import org.junit.Test; import java.util.Iterator; import java.util.List; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.StreamSupport; public class DummySequenceTest { @Test public void dummySequenceTest() { DummySequence<Integer> sequence = DummySequence.first(() -> { final int i = 10; return DummySequence.next(10, () -> { final int i1 = i + 10; return DummySequence.next(i1, () -> DummySequence.end(i1 + 10)); }); }); List<Integer> list = StreamSupport.stream(sequence.spliterator(), false) .collect(Collectors.toList()); Assert.assertEquals(10, ((int) list.get(0))); Assert.assertEquals(20, ((int) list.get(1))); Assert.assertEquals(30, ((int) list.get(2))); } private static class DummySequence<T> implements Iterable<T>, Iterator<T> { private Step<T> step; public DummySequence(Step<T> step) { this.step = step; } @Override public Iterator<T> iterator() { return this; } @Override public boolean hasNext() { if (step instanceof EndStep) return false; step = step.nextStep(); return true; } @Override public T next() { return step.getValue(); } public static <T> DummySequence<T> first(Supplier<Step<T>> next) { return new DummySequence<>(new FirstStep<T>(next)); } public static <T> Step<T> next(T value, Supplier<Step<T>> next) { return new IntermediateStep<>(value, next); } public static <T> Step<T> end(T value) { return new EndStep<>(value); } } private interface Step<T> { T getValue(); Step<T> nextStep(); } public static class FirstStep<T> implements Step<T> { Supplier<Step<T>> nextStep; public FirstStep(Supplier<Step<T>> next) { this.nextStep = next; } @Override public T getValue() { throw new IllegalStateException(); } @Override public Step<T> nextStep() { return nextStep.get(); } } public static class IntermediateStep<T> implements Step<T> { T value; Supplier<Step<T>> nextStep; public IntermediateStep(T value, Supplier<Step<T>> nextStep) { this.value = value; this.nextStep = nextStep; } @Override public T getValue() { return value; } @Override public Step<T> nextStep() { return nextStep.get(); } } public static class EndStep<T> implements Step<T> { T value; public EndStep(T value) { this.value = value; } @Override public T getValue() { return value; } @Override public Step<T> nextStep() { throw new IllegalStateException(); } } } 

Chaque lambda imbriqué suivant capture toutes les variables de tous les lambdas précédents et n'est exécuté que lorsque l'élément suivant est demandé. Le résultat de chaque lambda sera l'élément généré et le bloc de code suivant. Cela a l'air très étrange, et je doute que quelqu'un écrive comme ça. Nous dénotons l'idéal que nous viserons (et nous le réaliserons, sauf que nous utiliserons une classe anonyme au lieu de lambdas).


 Sequence<Integer> sequence = new Sequence<Integer>(() -> { int i = 10; yield(i); i += 10; yield(i); i += 10; yield(i); }); 

La fonction passée au constructeur Sequence ne doit passer de yield à yield que si nécessaire, les valeurs des variables locales doivent être stockées entre les appels à sequence.next() . Cette sauvegarde de la pile et du numéro de la dernière instruction exécutée est appelée préemption (le rendement est traduit en russe) ou suspension .


Continuation


Un morceau qui peut être évincé s'appelle Continuation. La suite est traduite en russe par «continuation», mais je l'appellerai continuation. Wikipedia écrit sur la suite:


Continuation (Eng. Continuation) représente l'état du programme à un certain moment, qui peut être enregistré et utilisé pour passer à cet état. Les suites contiennent toutes les informations pour continuer à exécuter un programme à partir d'un point spécifique.

Supposons que nous ayons déjà une sorte de manière magique implémentée le mécanisme de continuations, qui est représenté par l'interface suivante. La méthode run peut arrêter son exécution. Chaque appel suivant reprend l'exécution à partir du dernier yield . Nous pouvons considérer la continuation comme un Runnable qui peut être exécuté en plusieurs parties.


 interface Continuation<T> { void run(SequenceScope<T> scope); } 

Nous utiliserons la suite comme ceci:


 Sequence<Integer> sequence = new Sequence<>(new Continuation<>() { void run(SequenceScope<Integer> scope) { int i = 1; System.out.println("Continuation start"); scope.yield(i++); System.out.println("Continuation resume"); scope.yield(i++); System.out.println("Continuation resume"); scope.yield(i++); System.out.println("Continuation end"); } }); for(Integer i: sequence) { System.out.println("Next element :" + i); } 

Et nous nous attendons Ă  obtenir cette conclusion:


Sortie
 Continuation start Next element: 1 Continuation resume Next element: 2 Continuation resume Next element: 3 Continuation end 

Sequence à la demande de l'élément suivant appellera Continuation.run(scope) , qui exécutera un bloc de code jusqu'au prochain rendement et sera évincé. Le prochain appel à Continuation.run(scope) démarrera le travail à partir du dernier emplacement d'éviction et exécutera le code jusqu'au prochain yield . Sequence code de Sequence pourrait ressembler à ceci:


 class Sequence implements Iterator<T>, SequenceScope<T>, Iterable<T> { private static final Object STOP = new Object(); private Object next = STOP; private Continuation<T> nextStep; public Sequence(Continuation<T> nextStep) { this.nextStep = nextStep; } @Override public boolean hasNext() { if (next == STOP) { nextStep.run(this); } return next != STOP; } @Override public T next() { if (next == STOP) { if (!hasNext()) { throw new NoSuchElementException(); } } T result = (T) next; next = STOP; return result; } @Override void yield(T t) { next = t; } public Iterator<T> iterator() { //  ,       return this; } } interface SequenceScope<T> { void yield(T t); } 

Tout va bien, sauf que java n'est pas en mesure d'arrêter l'exécution de la méthode dans un endroit arbitraire, afin qu'il puisse ensuite continuer l'exécution à partir du lieu du dernier arrêt. Par conséquent, nous devrons le faire manuellement. Nous introduisons le champ label, dans lequel nous enregistrerons le numéro du dernier rendement appelé.


 class IntegerSequenceContinuation implements Continuation<Integer> { private int label = 0; private int i = 0; void run(SequenceScope<Integer> scope) { int i = this.i; switch (label) { case 0: System.out.println("Continuation start"); scope.yield(i++); label = 1; this.i = i; return; case 1: System.out.println("Continuation resume"); scope.yield(i++); label = 2; this.i = i; return; case 2: System.out.println("Continuation resume"); scope.yield(i++); label = 3; this.i = i; return; case 3: System.out.println("Continuation end"); label = 4; default: throw new RuntimeException(); } } } 

Nous avons une machine à états (machine à états finis), et dans l'ensemble c'est exactement ce que fait Kotlin dans ses coroutines (vous pouvez décompiler et voir si, bien sûr, vous comprenez quelque chose). Nous avons 4 états, chaque appel exécuté exécute un morceau de code et effectue la transition vers l'état suivant. Nous devons enregistrer la variable locale i dans le champ de classe. En plus d'une complexité injustifiée, ce code a un autre problème: nous pouvons passer différentes valeurs comme paramètre de portée pour chaque appel d'exécution. Par conséquent, il serait intéressant d'enregistrer le paramètre scope dans le champ de classe lors du premier appel et de continuer à travailler avec.


La continuation sur java est implémentée en nous, mais d'une manière assez étrange et dans un seul cas. Chaque fois que personne n'écrira quelque chose de similaire, éditer un tel code est difficile, lire un tel code est difficile. Par conséquent, nous allons construire la machine à états après la compilation.


Suspendable & Continuation


Comment pouvons-nous comprendre si la poursuite a terminé les travaux ou si elle a été suspendue? Laissez la méthode run retourner un objet SUSPEND spécial en cas de suspension.


 public interface Continuation<T> { Object SUSPEND = new Object() { @Override public String toString() { return "[SUSPEND]"; } }; T run(); } 

Notez que j'ai supprimé le paramètre d'entrée de la suite. Nous devons nous assurer que les paramètres ne changent pas d'un appel à l'autre, la meilleure façon de le faire est de les supprimer. L'utilisateur, au contraire, a besoin du paramètre scope (il sera utilisé pour beaucoup de choses, mais maintenant SequenceScope est passé à sa place, à partir de laquelle notre yield est appelé). De plus, l'utilisateur ne veut rien savoir de SUSPEND et ne veut rien retourner. Présentez l'interface Suspendable .


 public abstract class Suspendable<C extends Scope> { abstract public void run(C scope); } interface Scope {} 

Pourquoi une classe abstraite, pas une interface?

L'utilisation d'une classe au lieu d'une interface ne permet pas d'écrire des lambdas et force l'écriture de classes anonymes. Il sera très pratique pour nous de travailler en bytecode avec des continuations comme avec des classes, car les champs locaux peuvent être stockés dans leurs champs. Mais les lambdas en bytecode ne ressemblent pas à une classe. Pour plus de détails, rendez-vous ici .


Suspendable est Continuation au moment de la conception, tandis que Continuation est Suspendable à l' Suspendable . L'utilisateur écrit du code au niveau Suspendable et le code de bas niveau de la bibliothèque fonctionne avec Continuation . Il se transforme en un après la modification du bytecode.


Avant de parler de préemption après avoir appelé yield , mais à l'avenir, nous devrons préempter après d'autres méthodes. Nous marquerons ces méthodes avec l'annotation @Suspend . Cela s'applique pour se rendre:


 public class SequenceScope<T> implements Scope { @Suspend public void yield(T t) {...} } 

N'oubliez pas que nos suites seront construites sur des automates finis. Arrêtons-nous ici plus en détail. On l'appelle une machine à états finis car elle a un nombre fini d'états. Pour stocker l'état actuel, nous utiliserons un champ d'étiquette spécial. Initialement, étiquette est 0 - un état zéro (initial). Chaque appel à Continuation.run exécutera une sorte de code et entrera dans un état (dans un autre que celui initial). Après chaque transition, la suite doit enregistrer toutes les variables locales, le numéro d'état actuel et exécuter return SUSPEND . La transition vers l'état final sera dénotée par return null (dans les articles suivants, nous retournerons non seulement null ). Un appel à Continuation.run partir de l'état final doit se terminer par une exception ContinuationEndException .


Ainsi, l'utilisateur écrit le code dans Suspendable , après la compilation, il se transforme en Continuation , avec lequel la bibliothèque fonctionne, et, en particulier, nos générateurs. La création d'un nouveau générateur pour l'utilisateur ressemble à ceci:


 Sequence<Integer> seq = new Sequence(new Suspendable() {...}); 

Mais le générateur lui-même a besoin de continuation, car il doit initialiser le Continuation<T> nextStep; . Pour obtenir la Continuation de Suspendable dans le code, j'ai écrit une classe spéciale Magic .



 package microutine.core; import microutine.coroutine.CoroutineScopeImpl; import java.lang.reflect.Field; public class Magic { public static final String SCOPE = "scope$S"; private static <C extends Scope, R> Continuation<R> createContinuation(Suspendable<C> suspendable, C scope) { try { Field contextField = suspendable.getClass().getDeclaredField(SCOPE); contextField.setAccessible(true); if (contextField.get(suspendable) != null) throw new IllegalArgumentException("Continuation already created"); contextField.set(suspendable, scope); } catch (Exception e) { throw new RuntimeException(e); } return getContinuation(suspendable); } public static <R, C extends Scope> Continuation<R> getContinuation(Suspendable suspendable) { if (getScope(suspendable) == null) throw new RuntimeException("No continuation created for provided suspendable"); //noinspection unchecked return ((Continuation<R>) suspendable); } private static Scope getScope(Suspendable suspendable) { try { Field contextField = suspendable.getClass().getDeclaredField(SCOPE); contextField.setAccessible(true); return (Scope) contextField.get(suspendable); } catch (Exception e) { throw new RuntimeException(e); } } } 

Comment fonctionne cette magie? Avec le paramètre scope , le scope$S est initialisé par réflexion (le champ synthétique que nous allons créer dans le bytecode). Une continuation n'est initialisée qu'une seule fois dans createContinuation , et une deuxième tentative d'initialisation entraînera une exécution. Vient ensuite la conversion de type habituelle en Continuation . En général, je vous ai trompé, toute la magie n'est pas là. Puisqu'une telle conversion de type est possible, le Suspendable spécifique passé Suspendable implémente déjà Continuation . Et cela s'est produit lors de la compilation.


Structure du projet


Le projet comprendra trois parties:


  • Code de bibliothèque (API de bas niveau et de haut niveau)
  • Tests (En fait, seulement en eux maintenant vous pouvez utiliser cette bibliothèque)
  • Convertible Suspendable -> Continuation (implĂ©mentĂ© comme tâche gradle dans gradle buildSrc)

Comme le convertisseur est actuellement dans buildSrc, il sera impossible de l'utiliser quelque part en dehors de la bibliothèque elle-même. Mais pour l'instant, nous n'en avons pas besoin. À l'avenir, nous aurons deux options: en faire un plugin séparé, ou créer notre propre agent java (comme le fait Quasar ) et effectuer des transformations lors de l'exécution.


build.gradle
 plugins { id "java" } group 'microutines' version '1.0-SNAPSHOT' sourceCompatibility = 1.8 task processYield(type: microutine.ProcessSuspendableTask) { classPath = compileJava.outputs.files + compileJava.classpath inputs.files(compileJava.outputs.files) } task processTestYield(type: microutine.ProcessSuspendableTask) { classPath = compileJava.outputs.files + compileTestJava.classpath inputs.files(compileTestJava.outputs.files) } compileJava.finalizedBy(processYield) //      compileTestJava.finalizedBy(processTestYield) repositories { mavenCentral() } dependencies { testCompile group: 'junit', name: 'junit', version: '4.12' compile group: 'junit', name: 'junit', version: '4.12' } 

Suspendable to Continuation sera géré par la tâche TaskSuspendableTask. Il n'y a rien d'intéressant dans la classe de tâche grêle, elle sélectionne simplement les classes nécessaires et les envoie pour conversion en classe SuspendableConverter . C'est lui qui nous intéresse maintenant.


Génération de bytecode


Pour travailler avec le bytecode, nous utiliserons la bibliothèque OW2 ASM. La bibliothèque fonctionne sur le principe de l'analyseur SAX. Nous créons un nouveau ClassReader, lui donnons une classe compilée sous forme de tableau d'octets et appelons la méthode accept(ClassVisitor visitor) . ClassReader analysera le bytecode et appellera les méthodes appropriées sur le visiteur passé ( visitMethod , visitClass , visitInsn ). Le visiteur peut travailler en mode adaptateur et déléguer les appels au visiteur suivant. En règle générale, le dernier visiteur est un ClassWriter , dans lequel le bytecode final est généré. Si la tâche est non linéaire (nous n'en avons qu'une), elle peut prendre plusieurs passes dans la classe. Une autre approche fournie par asm consiste à écrire la classe dans un ClassNode spécial et à effectuer les transformations déjà sur celui-ci. La première approche est plus rapide, mais peut ne pas convenir à la résolution de problèmes non linéaires, j'ai donc utilisé les deux approches.


Suspendable y a 3 classes Suspendable dans la conversion de Suspendable en Continuation :


  • SuspendInfoCollector - analyse la mĂ©thode Suspendable.run , collecte des informations sur tous les appels aux mĂ©thodes @Suspend et sur les variables locales utilisĂ©es.
  • SuspendableConverter - crĂ©e les champs requis, modifie la signature et le handle de la mĂ©thode Suspendable.run pour obtenir Continuation.run .
  • SuspendableMethodConverter - Convertit le code de la mĂ©thode Suspendable.run . Ajoute un code pour enregistrer et restaurer les variables locales, enregistrer l'Ă©tat actuel dans le champ d' label et passer Ă  l'instruction souhaitĂ©e.

Décrivons quelques points plus en détail.


La recherche de la méthode d' run ressemble à ceci:


 MethodNode method = classNode.methods.stream() .filter(methodNode -> methodNode.name.equals("run") && (methodNode.access & Opcodes.ACC_BRIDGE) == 0) .findFirst() .orElseThrow(() -> new RuntimeException("Unable to find method to convert")); 

Il est prévu que dans la classe convertible, il y aura deux méthodes d' run , et l'une d'entre elles avec le modificateur de pont (ce que cela est lu ici ). Nous nous intéressons à une méthode sans modificateur.


Dans le bytecode JVM, une transition conditionnelle (et inconditionnelle) peut être effectuée n'importe où. ASM a une abstraction d' Label spéciale (étiquette), qui est une position dans le bytecode. Tout au long du code, après chaque appel aux méthodes @Suspend , nous @Suspend les étiquettes auxquelles nous effectuerons un saut conditionnel au début de la méthode run .


 @Override public void visitCode() { //    super.visitCode(); Label startLabel = new Label(); super.visitVarInsn(Opcodes.ALOAD, THIS_VAR_INDEX); //    this super.visitFieldInsn(Opcodes.GETFIELD, myClassJvmName, "label$S$S", "I"); //  label$S$S super.visitVarInsn(Opcodes.ISTORE, labelVarIndex); //      super.visitVarInsn(Opcodes.ILOAD, labelVarIndex); //   label   super.visitIntInsn(Opcodes.BIPUSH, 0); //  0   super.visitJumpInsn(Opcodes.IF_ICMPEQ, startLabel); //      startLabel        (label == 0) for (int i = 0; i < numLabels; i++) { //   ,     super.visitVarInsn(Opcodes.ILOAD, labelVarIndex); super.visitIntInsn(Opcodes.BIPUSH, i + 1); super.visitJumpInsn(Opcodes.IF_ICMPEQ, labels[i]); } super.visitTypeInsn(Opcodes.NEW, "microutine/core/ContinuationEndException"); // run      ,   super.visitInsn(Opcodes.DUP); super.visitMethodInsn(Opcodes.INVOKESPECIAL, "microutine/core/ContinuationEndException", "<init>", "()V", false); super.visitInsn(Opcodes.ATHROW); super.visitLabel(startLabel); // ,      } 

Nous @Suspend des étiquettes après les appels des méthodes @Suspend .


 @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { boolean suspendPoint = Utils.isSuspendPoint(classLoader, owner, name); super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); if (suspendPoint) { super.visitVarInsn(Opcodes.ALOAD, THIS_VAR_INDEX); //    this super.visitIntInsn(Opcodes.BIPUSH, suspensionNumber); //   ,       super.visitFieldInsn(Opcodes.PUTFIELD, myClassJvmName, "label$S$S", "I"); //     label$S$S saveFrame(); //    suspend(); super.visitLabel(labels[suspensionNumber - 1]); // ,     restoreFrame(); //    suspensionNumber++; } } private void suspend() { super.visitFieldInsn(Opcodes.GETSTATIC, "microutine/core/Continuation", "SUSPEND", "Ljava/lang/Object;"); //    Continuation.SUSPEND super.visitInsn(Opcodes.ARETURN); //   } 

Les tests


Nous écrivons un générateur qui donne trois nombres d'affilée.


testIntSequence
 public class YieldTest { @Test public void testIntSequence() { Sequence<Integer> sequence = new Sequence<Integer>(new SequenceSuspendable<Integer>() { @Override public void run(SequenceScope<Integer> scope) { scope.yield(10); scope.yield(20); scope.yield(30); } }); List<Integer> list = new ArrayList<>(); for (Integer integer : sequence) { list.add(integer); } assertEquals(10, (int) list.get(0)); assertEquals(20, (int) list.get(1)); assertEquals(30, (int) list.get(2)); } } 

Le test lui-même ne représente rien d'intéressant, mais il est suffisamment intéressant pour décompiler le fichier de classe.


testIntSequence décompilé
 public class YieldTest { public YieldTest() { } @Test public void testIntSequence() { class NamelessClass_1 extends SequenceSuspendable<Integer> implements Continuation { private SequenceScope scope$S; NamelessClass_1() { } public Object run(Object var1) { int label = this.label$S$S; SequenceScope var2; if (label != 0) { if (label != 1) { if (label != 2) { if (label != 3) { throw new ContinuationEndException(); } else { var2 = this.scope$S; this.label$S$S = 4; return null; } } else { var2 = this.scope$S; this.yield(30); this.label$S$S = 3; this.scope$S = var2; return Continuation.SUSPEND; } } else { var2 = this.scope$S; this.yield(20); this.label$S$S = 2; this.scope$S = var2; return Continuation.SUSPEND; } } else { var2 = this.scope$S; this.yield(10); this.label$S$S = 1; this.scope$S = var2; return Continuation.SUSPEND; } } } Sequence<Integer> sequence = new Sequence(new NamelessClass_1()); List<Integer> list = new ArrayList(); Iterator var3 = sequence.iterator(); while(var3.hasNext()) { Integer integer = (Integer)var3.next(); list.add(integer); } Assert.assertEquals(10L, (long)(Integer)list.get(0)); Assert.assertEquals(20L, (long)(Integer)list.get(1)); Assert.assertEquals(30L, (long)(Integer)list.get(2)); } } 

Le code est très gonflé, la plupart des instructions enregistrent et restaurent le cadre de pile (variables locales). Cependant, cela fonctionne. L'exemple donné fonctionnerait parfaitement sans génération paresseuse. Prenons un exemple plus difficile.


fibonacci
 public class YieldTest { @Test public void fibonacci() { Sequence<Integer> sequence = new Sequence<>(new Suspendable<Integer>() { @Override public void run(SequenceScope<Integer> scope) { scope.yield(1); scope.yield(1); int a = 1; int b = 1; while (true) { b += a; scope.yield(b); a += b; scope.yield(a); } } }); //noinspection OptionalGetWithoutIsPresent Integer tenthFibonacci = StreamSupport.stream(sequence.spliterator(), false) .skip(9).findFirst().get(); assertEquals(55, ((int) tenthFibonacci)); } } 

Le code ci-dessus génère une séquence de Fibonacci infinie. Nous compilons et décompilons:


fibonacci décompilé
 public class YieldTest { public YieldTest() { } @Test public void fibonacci() { class NamelessClass_1 extends SequenceSuspendable<Integer> implements Continuation { private SequenceScope scope$S; private int aa$S; private int ba$S; NamelessClass_1() { } public Object run(Object var1) { int label = this.label$S$S; SequenceScope var2; if (label != 0) { if (label != 1) { int var3; int var4; if (label != 2) { if (label == 3) { var2 = this.scope$S; var3 = this.aa$S; var4 = this.ba$S; var3 += var4; var2.yield(var3); this.label$S$S = 4; this.scope$S = var2; this.aa$S = var3; this.ba$S = var4; return Continuation.SUSPEND; } if (label != 4) { throw new ContinuationEndException(); } var2 = this.scope$S; var3 = this.aa$S; var4 = this.ba$S; } else { var2 = this.scope$S; var3 = 1; var4 = 1; } var4 += var3; var2.yield(var4); this.label$S$S = 3; this.scope$S = var2; this.aa$S = var3; this.ba$S = var4; return Continuation.SUSPEND; } else { var2 = this.scope$S; var2.yield(1); this.label$S$S = 2; this.scope$S = var2; return Continuation.SUSPEND; } } else { var2 = this.scope$S; var2.yield(1); this.label$S$S = 1; this.scope$S = var2; return Continuation.SUSPEND; } } } Sequence<Integer> sequence = new Sequence(new NamelessClass_1()); Integer tenthFibonacci = (Integer)StreamSupport.stream(sequence.spliterator(), false).skip(9L).findFirst().get(); Assert.assertEquals(55L, (long)tenthFibonacci); } } 

Comprendre ce qui rend une classe décompilée assez difficile. Comme la dernière fois, la plupart des instructions y conduisent des variables locales. Certaines affectations sont inutiles et les variables sont immédiatement effilochées par d'autres valeurs. , .


while, . . . , '' return SUSPEND .


Résumé


, , , . yield. , , — , . , ( ) . , JIT . yield yieldAll — , , , , . , , .


— — , . , . , : , .

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


All Articles