DIY Coroutines. Parte 1. Generadores perezosos

En el mundo de JVM, las corutinas son más conocidas gracias al lenguaje Kotlin y Project Loom . No he visto una buena descripción del principio de las corutinas de Kotlinovsky, y el código de la biblioteca de corotinas de Kotlin es completamente incomprensible para una persona no preparada. En mi experiencia, la mayoría de las personas solo saben acerca de las rutinas que se trata de "transmisiones ligeras" y que en Kotlin funcionan a través de la generación inteligente de códigos de bytes. Así lo fui hasta hace poco. Y se me ocurrió la idea de que, dado que las corutinas se pueden implementar en bytecode, ¿por qué no implementarlas en Java? A partir de esta idea, posteriormente, apareció una biblioteca pequeña y bastante simple, cuyo dispositivo, espero, pueda ser entendido por casi cualquier desarrollador. Detalles debajo del corte.



Código fuente


Llamé al proyecto Microutines, que proviene de las palabras Micro y Coroutines.


Todo el código está disponible en github . Inicialmente, quería construir la narrativa de acuerdo con el desarrollo de mi propia comprensión del tema, hablar sobre mis decisiones e ideas equivocadas, sobre el desarrollo de la API, pero luego el código en el artículo sería muy diferente del código con github (lo cual es inevitable en cualquier caso, escribo un artículo veces, y en github comedic de vez en cuando). Por lo tanto, describiré principalmente la versión final de la biblioteca, y si algunas partes de la API no son muy claras a primera vista, lo más probable es que sean necesarias para resolver los problemas que consideraremos en los siguientes artículos.


Descargo de responsabilidad


Este es un tipo de proyecto de capacitación. Me alegrará si uno de los lectores lo inclina a jugar o incluso lanzar una solicitud de extracción. Pero usarlo en producción no vale la pena. La mejor manera de comprender profundamente una tecnología es implementarla usted mismo, que es el único objetivo de este proyecto.


Y no garantizo la exactitud de los términos utilizados. Quizás escuché algunos de ellos, memorizados incorrectamente, y algunos incluso se me ocurrieron y olvidaron.


¿Qué son las corutinas?


Como ya he notado, a menudo se dice acerca de las corutinas que se trata de "flujos facilitados". Esta no es una definición verdadera. Tampoco daré una definición verdadera, pero trataré de describir cuáles son, corutinas. Llamar flujos de corutinas no será del todo correcto. Coroutine es una unidad de planificación más pequeña que una secuencia, y una secuencia, a su vez, es más pequeña que una unidad de planificación. La planificación de procesos y subprocesos es manejada por el sistema operativo. Corutin está comprometido en la planificación ... nosotros mismos participaremos en su planificación. Las rutinas funcionan por encima de los hilos regulares, y su truco principal es que no bloquean el hilo cuando esperan que se complete alguna otra tarea, sino que lo liberan para otra rutina. Este enfoque se llama multitarea cooperativa. Corutin puede funcionar primero en un hilo, luego en otro. Un hilo para la rutina actúa como un recurso, y un millón de corutina puede funcionar en un solo hilo. Podrías ver esta imagen:



Las tareas de contribs 1 y contribs 2 cumplen algún tipo de solicitud y no bloquean el flujo mientras esperan respuestas, sino que suspenden su trabajo y continúan después de recibir las respuestas. Podemos escribir dicho código usando devoluciones de llamada, usted dice. Eso es cierto, pero la esencia de la rutina es que escribimos código sin devoluciones de llamada, escribimos código secuencial ordinario que se ejecuta de forma asíncrona.


Generadores


Comenzaremos el desarrollo de simple a complejo. Lo primero que haremos es la generación de colecciones diferidas, implementada en algunos idiomas usando la palabra clave de rendimiento. Los generadores no son accidentales aquí, como veremos más adelante, y los generadores y las corutinas pueden implementarse utilizando el mismo mecanismo.


Consideremos un ejemplo en python, simplemente porque los generadores están listos para usar.


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

El ciclo se desarrolla en algo como esto (tal vez no del todo así, pero el principio es importante para nosotros):


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

Una llamada a generator() creará un iterador especial llamado generador. La primera llamada a next(gen) ejecuta el código desde el comienzo de la función del generator hasta el primer yield , y el valor de la variable local k del genertator() escribe en la variable i . Cada llamada siguiente al next continuará ejecutando la función con la instrucción que sigue inmediatamente al yield anterior yield y así sucesivamente. En este caso, entre las next llamadas, se guardan los valores de todas las variables locales dentro del generator .


Eso es casi lo mismo, pero en el idioma 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, podríamos hacer una generación perezosa como esta:


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

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

Cada siguiente lambda anidada captura todas las variables de todas las lambdas anteriores y se ejecuta solo cuando se solicita el siguiente elemento. El resultado de cada lambda será el elemento generado y el siguiente bloque de código. Se ve muy extraño, y dudo que alguien escriba así. Denotamos el ideal por el que lucharemos (y lo lograremos, excepto que usaremos una clase anónima en lugar de lambdas).


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

La función pasada al constructor de secuencia debe ir de yield a yield solo si es necesario, los valores de las variables locales deben almacenarse entre las llamadas a la sequence.next() . Este ahorro de la pila y el número de la última instrucción ejecutada se llama preferencia (el rendimiento se traduce al ruso) o suspensión .


Continuación


Una pieza que puede exprimirse se llama Continuación. La continuación se traduce al ruso como 'continuación', pero lo llamaré continuación. Wikipedia escribe sobre la continuación:


La continuación (Ing. Continuación) representa el estado del programa en un momento determinado, que se puede guardar y utilizar para hacer la transición a este estado. Las continuaciones contienen toda la información para continuar ejecutando un programa desde un punto específico.

Supongamos que ya tenemos algún tipo de forma mágica implementado el mecanismo de continuación, que está representado por la siguiente interfaz. El método de run puede detener su ejecución. Cada llamada subsiguiente reanuda la ejecución desde el último yield . Podemos pensar en la continuación como un Runnable que se puede ejecutar en partes.


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

Usaremos la continuación así:


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

Y esperamos llegar a esta conclusión:


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

Sequence a petición del siguiente elemento llamará a Continuation.run(scope) , que ejecutará un bloque de código hasta el próximo rendimiento y se desplazará. La próxima llamada a Continuation.run(scope) comenzará a funcionar desde el lugar del último desplazamiento y ejecutará el código hasta el siguiente yield . Sequence código de Sequence podría ser así:


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

Todo está bien, excepto que Java no puede detener la ejecución del método en un lugar arbitrario, por lo que puede continuar la ejecución desde el lugar de la última parada. Por lo tanto, tendremos que hacer esto manualmente. Ingrese el campo de etiqueta en el que almacenaremos el número del último rendimiento llamado.


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

Tenemos una máquina de estados (máquina de estados finitos) y, en general, esto es exactamente lo que Kotlin hace en sus corutinas (puedes descompilar y ver si, por supuesto, entiendes algo). Tenemos 4 estados, cada llamada de run ejecuta un fragmento de código y realiza la transición al siguiente estado. Tenemos que guardar la variable local i en el campo de clase. Además de la complejidad injustificada, este código tiene otro problema: podemos pasar diferentes valores como parámetro de alcance para cada llamada de ejecución. Por lo tanto, sería bueno guardar el parámetro de alcance en el campo de clase en la primera llamada y continuar trabajando con él.


La continuación en Java se implementa en nosotros, pero de una manera bastante extraña y en una sola instancia. Cada vez que nadie escribirá algo similar, editar dicho código es difícil, leer dicho código es difícil. Por lo tanto, construiremos la máquina de estado después de la compilación.


Suspendible y Continuación


¿Cómo entendemos si la continuación ha completado el trabajo o se ha suspendido? Deje que el método de run devuelva un objeto SUSPEND especial en caso de suspensión.


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

Tenga en cuenta que eliminé el parámetro de entrada de la continuación. Debemos asegurarnos de que los parámetros no cambien de una llamada a otra, la mejor manera de hacerlo es eliminarlos. El usuario, por el contrario, necesita el parámetro de scope (se usará para muchas cosas, pero ahora SequenceScope se pasa a su lugar, desde donde se llama nuestro yield ). Además, el usuario no quiere saber nada de SUSPEND y no quiere devolver nada. Introduce la interfaz Suspendable .


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

¿Por qué una clase abstracta, no una interfaz?

Usar una clase en lugar de una interfaz no permite escribir lambdas y obliga a escribir clases anónimas. Será muy conveniente para nosotros trabajar en bytecode con continuaciones como con las clases, porque los campos locales pueden almacenarse en sus campos. Pero las lambdas en bytecode no parecen una clase. Para más detalles, vaya aquí .


Suspendable es Continuation en tiempo de diseño, mientras que Continuation es Suspendable en Suspendable de Suspendable . El usuario escribe código en el nivel Suspendable , y el código de bajo nivel de la biblioteca funciona con Continuation . Se convierte en uno después de la modificación del código de bytes.


Antes hablábamos de adelantarnos después de llamar a yield , pero en el futuro tendremos que adelantarnos después de algunos otros métodos. @Suspend dichos métodos con la anotación @Suspend . Esto se aplica al yield sí mismo:


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

Recuerde que nuestras continuaciones se construirán sobre autómatas finitos. Detengámonos aquí con más detalle. Se llama máquina de estados finitos porque tiene un número finito de estados. Para almacenar el estado actual, utilizaremos un campo de etiqueta especial. Inicialmente, etiqueta es 0: un estado cero (inicial). Cada llamada a Continuation.run ejecutará algún tipo de código y entrará en algún estado (en cualquier otro que no sea el inicial). Después de cada transición, la continuación debe guardar todas las variables locales, el número de estado actual y ejecutar return SUSPEND . La transición al estado final se indicará mediante return null (en los siguientes artículos no solo devolveremos null ). Una llamada a Continuation.run desde el estado final debe finalizar con una excepción ContinuationEndException .


Entonces, el usuario escribe el código en Suspendable , después de la compilación se convierte en Continuation , con el que funciona la biblioteca y, en particular, nuestros generadores. La creación de un nuevo generador para el usuario se ve así:


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

Pero el generador mismo necesita continuación, porque necesita inicializar el campo Continuation<T> nextStep; . Para obtener la Continuation de Suspendable en el código, escribí una clase especial de 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); } } } 

¿Cómo funciona esta magia? Con el parámetro de scope , el scope$S se inicializa mediante reflexión (el campo sintético que crearemos en el código de bytes). Una continuación se inicializa solo una vez en createContinuation , y un segundo intento de inicialización dará como resultado una ejecución. Luego viene el tipo habitual lanzado a Continuation . En general, te engañé, toda la magia no está aquí. Como tal conversión de tipo es posible, el Suspendable específico pasado ya implementa la Continuation . Y esto sucedió durante la compilación.


Estructura del proyecto


El proyecto constará de tres partes:


  • Código de biblioteca (API de bajo y alto nivel)
  • Pruebas (de hecho, solo en ellas ahora puedes usar esta biblioteca)
  • Convertidor Suspendible -> Continuación (Implementado como tarea gradle en gradle buildSrc)

Como el convertidor está actualmente en buildSrc, será imposible usarlo en algún lugar fuera de la biblioteca. Pero por ahora, no lo necesitamos. En el futuro, tendremos dos opciones: crear un complemento por separado o crear nuestro propio agente de Java (como lo hace Quasar ) y realizar transformaciones en tiempo de ejecución.


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 a Continuation será manejado por la tarea TaskSuspendableTask. No hay nada interesante en la clase de tarea de granizo, solo selecciona las clases necesarias y las envía para su conversión a la clase SuspendableConverter . Es él quien ahora nos interesa.


Generación de Bytecode


Para trabajar con bytecode, utilizaremos la biblioteca OW2 ASM. La biblioteca funciona según el principio del analizador SAX. Creamos un nuevo ClassReader, lo alimentamos con una clase compilada como una matriz de bytes y llamamos al método accept(ClassVisitor visitor) . ClassReader analizará el código de byte y llamará a los métodos apropiados en el visitante pasado ( visitMethod , visitClass , visitInsn ). El visitante puede trabajar en modo adaptador y delegar llamadas al siguiente visitante. Normalmente, el último visitante es un ClassWriter , en el que se genera el bytecode final. Si la tarea no es lineal (solo tenemos una), puede tomar varios pases por la clase. Otro enfoque proporcionado por asm es escribir la clase en un ClassNode especial y hacer las transformaciones que ya están en él. El primer enfoque es más rápido, pero puede no ser adecuado para resolver problemas no lineales, por lo que utilicé ambos enfoques.


Suspendable 3 clases Suspendable en la conversión de Suspendable a Continuation :


  • SuspendInfoCollector : analiza el método Suspendable.run , recopila información sobre todas las llamadas a los métodos @Suspend y sobre las variables locales utilizadas.
  • SuspendableConverter : crea los campos obligatorios, cambia la firma y el identificador del método Suspendable.run para obtener Continuation.run .
  • SuspendableMethodConverter : convierte el código del método Suspendable.run . Agrega un código para guardar y restaurar variables locales, guardar el estado actual en el campo de label y pasar a la instrucción deseada.

Describamos algunos puntos con más detalle.


La búsqueda del método de run se ve así:


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

Se espera que en la clase convertible haya dos métodos de run , y uno de ellos con el modificador de puente (lo que se lee aquí ). Estamos interesados ​​en un método sin un modificador.


En el código de bytes JVM, se puede realizar una transición condicional (e incondicional) en cualquier lugar. ASM tiene una abstracción de Label especial (etiqueta), que es una posición en el código de bytes. A lo largo del código, después de cada llamada a los métodos @Suspend , @Suspend las etiquetas a las que haremos un salto condicional al comienzo del método de 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); // ,      } 

@Suspend etiquetas después de las llamadas de los métodos @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); //   } 

Pruebas


Escribimos un generador que da tres números seguidos.


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

La prueba en sí no representa nada interesante, pero es lo suficientemente interesante como para descompilar el archivo de clase.


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

El código es muy hinchado, la mayoría de las instrucciones son guardar y restaurar el marco de la pila (variables locales). Sin embargo, funciona. El ejemplo dado funcionaría perfectamente sin una generación perezosa. Consideremos un ejemplo más difícil.


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

El código anterior genera una secuencia infinita de Fibonacci. Compilamos y descompilamos:


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

Comprender lo que hace que una clase descompilada sea lo suficientemente difícil. Como la última vez, la mayoría de las instrucciones manejan variables locales allí. Algunas de las asignaciones son inútiles, y las variables son inmediatamente deshilachadas por otros valores. , .


while, . . . , '' return SUSPEND .


Resumen


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


— — , . , . , : , .

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


All Articles