DIY Coroutinen. Teil 1. Lazy Generators

In der JVM-Welt sind Coroutinen dank der Kotlin-Sprache und Project Loom am bekanntesten. Ich habe keine gute Beschreibung des Prinzips der Kotlinovsky-Koroutinen gesehen, und der Code der Kotlin-Koroutinen-Bibliothek ist für eine unvorbereitete Person völlig unverständlich. Nach meiner Erfahrung wissen die meisten Leute nur über Coroutinen, dass es sich um "Lightweight Streams" handelt und dass sie in Kotlin durch intelligente Bytecode-Generierung arbeiten. So war ich bis vor kurzem. Da Coroutinen in Bytecode implementiert werden können, kam mir die Idee, sie in Java zu implementieren. Aus dieser Idee entstand später eine kleine und recht einfache Bibliothek, deren Gerät hoffentlich von fast jedem Entwickler verstanden werden kann. Details unter dem Schnitt.



Quellcode


Ich habe das Projekt Microutines genannt, das von den Wörtern Micro und Coroutines abstammt.


Der gesamte Code ist auf Github verfügbar. Anfangs wollte ich die Erzählung in Übereinstimmung mit der Entwicklung meines eigenen Verständnisses des Themas aufbauen, über meine falschen Entscheidungen und Ideen sprechen, über die Entwicklung von API, aber dann würde sich der Code im Artikel sehr vom Code mit Github unterscheiden (was auf jeden Fall unvermeidlich ist, ich schreibe einen Artikel) und von Zeit zu Zeit auf Github Comedic). Daher beschreibe ich hauptsächlich die endgültige Version der Bibliothek. Wenn einige Teile der API auf den ersten Blick nicht klar sind, werden sie höchstwahrscheinlich benötigt, um die in den folgenden Artikeln behandelten Probleme zu lösen.


Haftungsausschluss


Dies ist eine Art Trainingsprojekt. Ich würde mich freuen, wenn einer der Leser ihn zum Spielen oder sogar zum Ziehen auffordert. Aber die Verwendung in der Produktion lohnt sich nicht. Der beste Weg, eine Technologie zu verstehen, besteht darin, sie selbst zu implementieren. Dies ist das einzige Ziel dieses Projekts.


Und ich übernehme keine Garantie für die Richtigkeit der verwendeten Begriffe. Vielleicht haben sich einige von ihnen, die ich irgendwo gehört habe, falsch eingeprägt, und einige von ihnen haben sich sogar etwas ausgedacht und es vergessen.


Was sind Koroutinen?


Wie ich bereits bemerkt habe, wird oft über Koroutinen gesagt, dass dies „erleichterte Flüsse“ sind. Dies ist keine wahre Definition. Ich werde auch keine wahre Definition geben, aber ich werde versuchen zu beschreiben, was sie sind, Koroutinen. Das Aufrufen von Coroutine-Flows ist nicht ganz korrekt. Coroutine ist eine kleinere Planungseinheit als ein Stream, und ein Stream ist wiederum kleiner als eine Planungseinheit. Die Prozess- und Thread-Planung wird vom Betriebssystem übernommen. Corutin ist in die Planung involviert ... wir selbst werden in die Planung involviert sein. Coroutinen arbeiten auf regulären Threads, und ihr Haupttrick besteht darin, dass sie den Thread nicht blockieren, wenn sie auf den Abschluss einer anderen Aufgabe warten, sondern ihn für eine andere Coroutine freigeben. Dieser Ansatz wird als kooperatives Multitasking bezeichnet. Corutin kann zuerst in einem Thread und dann in einem anderen Thread arbeiten. Ein Thread für Coroutine fungiert als Ressource, und eine Million Coroutine kann an einem einzelnen Thread arbeiten. Sie konnten dieses Bild sehen:



Die Aufgaben von Beitrag 1 und Beitrag 2 erfüllen eine Art von Anforderungen und blockieren den Fluss nicht, während sie auf Antworten warten, sondern unterbrechen ihre Arbeit und setzen sie fort, nachdem sie die Antworten erhalten haben. Wir können solchen Code mit Rückrufen schreiben, sagen Sie. Das ist wahr, aber das Wesen der Coroutine ist, dass wir Code ohne Rückrufe schreiben, wir schreiben gewöhnlichen sequentiellen Code, der asynchron läuft.


Generatoren


Wir beginnen mit der Entwicklung von einfach bis komplex. Das erste, was wir tun werden, ist die verzögerte Erstellung von Sammlungen, die in einigen Sprachen mit dem Schlüsselwort yield implementiert werden. Generatoren sind hier nicht zufällig, wie wir später sehen werden, und Generatoren und Coroutinen können unter Verwendung des gleichen Mechanismus implementiert werden.


Betrachten wir ein Beispiel in Python, einfach weil die Generatoren sofort einsatzbereit sind.


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

Der Zyklus entfaltet sich in so etwas (vielleicht nicht ganz so, aber das Prinzip ist uns wichtig):


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

Ein Aufruf von generator() erzeugt einen speziellen Iterator namens generator. Der erste Aufruf von next(gen) führt den Code vom Beginn der generator bis zur ersten genertator() aus, und der Wert der lokalen Variablen k von genertator() in die Variable i . Bei jedem nächsten Aufruf von next wird die Funktion mit der Anweisung ausgeführt, die unmittelbar auf die vorherige yield folgt yield und so weiter. In diesem Fall werden zwischen den next Aufrufen die Werte aller lokalen Variablen im generator gespeichert.


Das ist ungefähr das Gleiche, aber in der Kotlin-Sprache.


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

In Java könnten wir so etwas faul generieren:


 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); } 

DummySequence-Implementierung
 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(); } } } 

Jedes nächste verschachtelte Lambda erfasst alle Variablen aus allen vorherigen Lambdas und wird nur ausgeführt, wenn das nächste Element angefordert wird. Das Ergebnis jedes Lambdas ist das erzeugte Element und der nächste Codeblock. Es sieht sehr seltsam aus und ich bezweifle, dass jemand so schreiben wird. Wir bezeichnen das Ideal, nach dem wir streben werden (und wir werden es erreichen, außer dass wir eine anonyme Klasse anstelle von Lambdas verwenden).


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

Die an den Sequence-Konstruktor übergebene Funktion sollte nur bei Bedarf von yield zu yield wechseln. Die Werte lokaler Variablen sollten zwischen den Aufrufen von sequence.next() gespeichert werden. Diese Speicherung des Stacks und der Nummer des zuletzt ausgeführten Befehls wird als Preemption (Ausbeute wird ins Russische übersetzt) ​​oder Suspension bezeichnet .


Fortsetzung


Ein Stück, das herausgedrückt werden kann, heißt Fortsetzung. Fortsetzung wird ins Russische übersetzt als "Fortsetzung", aber ich werde es Fortsetzung nennen. Wikipedia schreibt über die Fortsetzung:


Fortsetzung (engl. Continuation) repräsentiert den Zustand des Programms zu einem bestimmten Zeitpunkt, der gespeichert und zum Übergang in diesen Zustand verwendet werden kann. Fortsetzungen enthalten alle Informationen, um die Ausführung eines Programms ab einem bestimmten Punkt fortzusetzen.

Angenommen, wir haben bereits auf magische Weise den Mechanismus der Fortsetzungen implementiert, der durch die folgende Schnittstelle dargestellt wird. Die run Methode kann die Ausführung stoppen. Jeder nachfolgende Aufruf setzt die Ausführung von der letzten yield . Wir können uns die Fortsetzung als ein Runnable , das in Teilen ausgeführt werden kann.


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

Wir werden die Fortsetzung folgendermaßen verwenden:


 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); } 

Und wir erwarten diese Schlussfolgerung:


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

Sequence ruft auf Anforderung des nächsten Elements Continuation.run(scope) , das einen Codeblock bis zur nächsten Ausgabe ausführt und verdrängt. Der nächste Aufruf von Continuation.run(scope) startet die Arbeit am Ort des letzten Verdrängens und führt den Code bis zur nächsten yield . Sequence könnte folgendermaßen aussehen:


 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); } 

Alles ist in Ordnung, außer dass Java nicht in der Lage ist, die Ausführung der Methode an einer beliebigen Stelle zu stoppen, so dass die Ausführung an der Stelle des letzten Stopps fortgesetzt werden kann. Daher müssen wir dies manuell tun. Wir führen das Beschriftungsfeld ein, in dem wir die Nummer des zuletzt aufgerufenen Ertrags speichern.


 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(); } } } 

Wir haben eine Zustandsmaschine (finite state machine), und im Großen und Ganzen macht Kotlin genau das in seinen Coroutinen (Sie können dekompilieren und sehen, ob Sie natürlich etwas verstehen). Wir haben 4 Zustände, jeder run führt einen Teil des Codes aus und führt den Übergang zum nächsten Zustand durch. Wir müssen die lokale Variable i im Klassenfeld speichern. Neben der ungerechtfertigten Komplexität weist dieser Code ein weiteres Problem auf: Wir können für jeden Ausführungsaufruf verschiedene Werte als Bereichsparameter übergeben. Daher wäre es schön, den scope-Parameter beim ersten Aufruf im class-Feld zu speichern und weiter damit zu arbeiten.


Die Fortsetzung auf Java ist in uns implementiert, aber auf seltsame Weise und nur in einem Fall. Jedes Mal, wenn niemand etwas Ähnliches schreibt, ist es schwierig, einen solchen Code zu bearbeiten, und es ist schwierig, einen solchen Code zu lesen. Daher werden wir die Zustandsmaschine nach der Kompilierung erstellen.


Suspendable & Fortsetzung


Wie verstehen wir, wenn die Fortsetzung der Arbeit abgeschlossen oder ausgesetzt wurde? Lassen Sie die run Methode im Falle einer SUSPEND ein spezielles SUSPEND Objekt zurückgeben.


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

Beachten Sie, dass ich den Eingabeparameter aus der Fortsetzung entfernt habe. Wir müssen sicherstellen, dass sich die Parameter nicht von Aufruf zu Aufruf ändern. Am besten löschen Sie sie. Der Benutzer benötigt im Gegenteil den scope Parameter (er wird für viele Dinge verwendet, aber jetzt wird SequenceScope an die Stelle übergeben, von der aus unsere yield aufgerufen wird). Außerdem möchte der Benutzer nichts über SUSPEND wissen und nichts zurückgeben. Suspendable die Suspendable Oberfläche vor.


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

Warum eine abstrakte Klasse, keine Schnittstelle?

Die Verwendung einer Klasse anstelle einer Schnittstelle ermöglicht das Schreiben von Lambdas nicht und erzwingt das Schreiben anonymer Klassen. Es ist für uns sehr praktisch, mit Fortsetzungen wie mit Klassen im Bytecode zu arbeiten, da lokale Felder in ihren Feldern gespeichert werden können. Aber Lambdas im Bytecode sehen nicht wie eine Klasse aus. Einzelheiten finden Sie hier .


Suspendable ist " Continuation in der Entwurfszeit, " Continuation ist " Suspendable in der Suspendable . Der Benutzer schreibt Code auf der Ebene Suspendable , und der Code auf niedriger Ebene der Bibliothek arbeitet mit Continuation . Es wird eins nach der Änderung des Bytecodes.


Früher haben wir über das Preempting nach dem Aufruf von yield , aber in Zukunft müssen wir nach einigen anderen Methoden preempting. Wir werden solche Methoden mit @Suspend Annotation @Suspend . Dies gilt für die yield selbst:


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

Denken Sie daran, dass unsere Fortsetzungen auf endlichen Automaten basieren. Lassen Sie uns hier näher eingehen. Es wird eine endliche Zustandsmaschine genannt, weil es eine endliche Anzahl von Zuständen hat. Um den aktuellen Status zu speichern, verwenden wir ein spezielles Beschriftungsfeld. Anfänglich ist Beschriftungsfeld 0 - ein Nullzustand (Anfangszustand). Jeder Aufruf von Continuation.run führt eine Art Code aus und wechselt in einen anderen Status (als den ursprünglichen). Nach jedem Übergang sollte die Fortsetzung alle lokalen Variablen und die aktuelle return SUSPEND und die return SUSPEND ausführen. Der Übergang in den Endzustand wird mit return null (in den folgenden Artikeln wird nicht nur null ). Ein Aufruf von Continuation.run aus dem Endzustand sollte mit einer ContinuationEndException Ausnahme enden.


Der Benutzer schreibt den Code also in Suspendable , und nach dem Kompilieren wird daraus Continuation , mit der die Bibliothek und insbesondere unsere Generatoren arbeiten. Das Erstellen eines neuen Generators für den Benutzer sieht folgendermaßen aus:


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

Der Generator selbst benötigt jedoch eine Fortsetzung, da er das Continuation<T> nextStep; initialisieren Continuation<T> nextStep; . Um die Continuation von Suspendable im Code zu erhalten, habe ich eine spezielle Magic Klasse geschrieben.



 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); } } } 

Wie funktioniert diese Magie? Mit dem scope Parameter wird das scope$S Feld über Reflection initialisiert (das synthetische Feld, das wir im Bytecode erstellen). Eine Fortsetzung wird in createContinuation nur einmal createContinuation , und ein zweiter Initialisierungsversuch führt zur Ausführung. Als nächstes folgt die übliche Typumwandlung zur Continuation . Im Allgemeinen habe ich dich getäuscht, die ganze Magie ist nicht hier. Da eine solche Typkonvertierung möglich ist, implementiert das speziell übergebene Suspendable bereits die Continuation . Und das geschah während der Kompilierung.


Projektstruktur


Das Projekt wird aus drei Teilen bestehen:


  • Bibliothekscode (Low Level und High Level API)
  • Tests (In der Tat, nur in ihnen können Sie jetzt diese Bibliothek verwenden)
  • Konverter Suspendable -> Continuation (Implementiert als Gradle-Task in Gradle BuildSrc)

Da sich der Konverter derzeit in buildSrc befindet, kann er nicht außerhalb der Bibliothek selbst verwendet werden. Aber im Moment brauchen wir es nicht. In Zukunft werden wir zwei Möglichkeiten haben: ein separates Plugin oder einen eigenen Java-Agenten (wie Quasar ) und Transformationen zur Laufzeit.


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 wird von der Task TaskSuspendableTask behandelt. Es gibt nichts Interessantes in der Hagel-Task-Klasse, sie wählt nur die erforderlichen Klassen aus und sendet sie zur Konvertierung an die SuspendableConverter Klasse. Er interessiert uns jetzt.


Bytecode-Generierung


Für die Arbeit mit Bytecode verwenden wir die OW2-ASM-Bibliothek. Die Bibliothek arbeitet nach dem Prinzip des SAX-Parsers. Wir erstellen einen neuen ClassReader, geben ihm eine kompilierte Klasse als Array von Bytes und rufen die accept(ClassVisitor visitor) Methode accept(ClassVisitor visitor) . ClassReader analysiert den Bytecode und ruft die entsprechenden Methoden für den übergebenen Besucher auf ( visitMethod , visitClass , visitInsn ). Der Besucher kann im Adaptermodus arbeiten und Anrufe an den nächsten Besucher delegieren. In der Regel ist der letzte Besucher ein ClassWriter , in dem der endgültige Bytecode generiert wird. Wenn die Aufgabe nicht linear ist (wir haben nur eine), kann sie mehrere Durchgänge durch die Klasse dauern. Ein weiterer Ansatz von asm besteht darin, die Klasse in einen speziellen ClassNode zu schreiben und die bereits darin enthaltenen Transformationen ClassNode . Der erste Ansatz ist schneller, ist jedoch möglicherweise nicht für die Lösung nichtlinearer Probleme geeignet. Daher habe ich beide Ansätze verwendet.


Suspendable gibt 3 Klassen bei der Konvertierung von Suspendable in Continuation :


  • SuspendInfoCollector - analysiert die Suspendable.run Methode, sammelt Informationen zu allen Aufrufen der @Suspend Methoden und zu den verwendeten lokalen Variablen.
  • SuspendableConverter - SuspendableConverter die erforderlichen Felder, ändert die Signatur und das Handle der Suspendable.run Methode, um Continuation.run Suspendable.run .
  • SuspendableMethodConverter - Konvertiert den Code der Suspendable.run Methode. Fügt einen Code zum Speichern und Wiederherstellen lokaler Variablen hinzu, speichert den aktuellen Status im label und wechselt zur gewünschten Anweisung.

Lassen Sie uns einige Punkte genauer beschreiben.


Die Suche nach der run Methode sieht folgendermaßen aus:


 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")); 

Es wird erwartet, dass es in der Convertible-Klasse zwei run gibt, eine davon mit dem Bridge-Modifikator (was hier gelesen wird). Wir sind an einer Methode ohne Modifikator interessiert.


Im JVM-Bytecode kann überall ein bedingter (und unbedingter) Übergang ausgeführt werden. ASM verfügt über eine spezielle Label Abstraktion (Label), die eine Position im Bytecode darstellt. Nach jedem Aufruf der @Suspend Methoden platzieren wir im gesamten Code die Bezeichnungen, zu denen wir am Anfang der run Methode einen bedingten Sprung 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); // ,      } 

Wir platzieren Labels nach Aufrufen von @Suspend Methoden.


 @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); //   } 

Tests


Wir schreiben einen Generator, der drei Zahlen hintereinander gibt.


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)); } } 

Der Test selbst stellt nichts Interessantes dar, ist aber interessant genug, um die Klassendatei zu dekompilieren.


testIntSequence dekompiliert
 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)); } } 

Der Code ist sehr aufgebläht, die meisten Anweisungen speichern den Stack-Frame (lokale Variablen) und stellen ihn wieder her. Es funktioniert jedoch. Das gegebene Beispiel würde ohne faule Erzeugung tadellos funktionieren. Betrachten wir ein Beispiel als schwieriger.


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)); } } 

Der obige Code erzeugt eine unendliche Fibonacci-Sequenz. Wir kompilieren und dekompilieren:


Fibonacci dekompiliert
 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); } } 

Verstehen, was eine dekompilierte Klasse schwierig genug macht. Wie beim letzten Mal steuern die meisten Anweisungen dort lokale Variablen. Einige der Zuweisungen sind unbrauchbar und Variablen werden sofort von anderen Werten ausgefranst. , .


while, . . . , '' return SUSPEND .


Zusammenfassung


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


— — , . , . , : , .

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


All Articles