Corotinas DIY. Parte 1. Geradores Preguiçosos

No mundo da JVM, as corotinas são mais conhecidas graças à linguagem Kotlin e ao Project Loom . Não vi uma boa descrição do princípio das corotinas de Kotlinovsky, e o código da biblioteca de corotinas-corotinas é completamente incompreensível para uma pessoa despreparada. Na minha experiência, a maioria das pessoas sabe apenas sobre as corotinas que esses são "fluxos leves" e que no kotlin eles trabalham através da geração inteligente de bytecodes. Então eu estava até recentemente. E surgiu a ideia de que, como as corotinas podem ser implementadas no bytecode, por que não implementá-las em java? A partir dessa idéia, subseqüentemente, apareceu uma pequena e bastante simples biblioteca, cujo dispositivo, espero, pode ser entendido por quase qualquer desenvolvedor. Detalhes sob o corte.



Código fonte


Chamei o projeto Microutines, que veio das palavras Micro e Coroutines.


Todo o código está disponível no github . Inicialmente, eu queria construir a narrativa de acordo com o desenvolvimento do meu próprio entendimento do tópico, falar sobre minhas decisões e idéias erradas, sobre o desenvolvimento da API, mas o código no artigo seria muito diferente do código com o github (que é inevitável em qualquer caso, escrevo um artigo vezes e no cômico do github de tempos em tempos). Portanto, descreverei principalmente a versão final da biblioteca e, se algumas partes da API não forem muito claras à primeira vista, provavelmente serão necessárias para resolver os problemas que consideraremos nos artigos a seguir.


Isenção de responsabilidade


Este é um tipo de projeto de treinamento. Ficarei feliz se um dos leitores o inclinar a jogar ou mesmo fazer um pedido de recepção. Mas usá-lo na produção não vale a pena. A melhor maneira de entender profundamente uma tecnologia é implementá-la, que é o único objetivo deste projeto.


E não garanto a precisão dos termos usados. Talvez alguns deles eu tenha ouvido em algum lugar, memorizado incorretamente, e alguns até tenham inventado comigo mesmo e esquecido.


O que são corotinas


Como já notei, é frequentemente dito sobre as corotinas que esses são "fluxos facilitados". Esta não é uma definição verdadeira. Também não darei uma definição verdadeira, mas tentarei descrever o que são, corotinas. Chamar fluxos de corotinas não estará totalmente correto. A coroutina é uma unidade de planejamento menor que um fluxo, e um fluxo, por sua vez, é menor que uma unidade de programação. O planejamento de processos e encadeamentos é tratado pelo sistema operacional. Corutin está envolvido no planejamento ... nós mesmos estaremos envolvidos no planejamento deles. As corotinas funcionam em cima dos encadeamentos regulares, e seu principal truque é que eles não bloqueiam o encadeamento quando esperam que outra tarefa seja concluída, mas o liberam para outra corotina. Essa abordagem é chamada de multitarefa cooperativa. Corutin pode trabalhar primeiro em um segmento, depois em outro. Um encadeamento para corotina atua como um recurso, e um milhão de corotinas podem funcionar em um único encadeamento. Você pode ver esta imagem:



As tarefas dos contributos 1 e 2 contribuem para algum tipo de solicitação e não bloqueiam o fluxo enquanto aguarda respostas, mas suspendem o trabalho e continuam após receber as respostas. Podemos escrever esse código usando retornos de chamada, você diz. Isso é verdade, mas a essência da corotina é que escrevemos código sem retornos de chamada, escrevemos código sequencial comum que é executado de forma assíncrona.


Geradores


Começaremos o desenvolvimento do simples ao complexo. A primeira coisa que faremos é a geração lenta de coleções, implementada em alguns idiomas usando a palavra-chave yield. Os geradores não são acidentais aqui, como veremos mais adiante, e geradores e corotinas podem ser implementados usando o mesmo mecanismo.


Vamos considerar um exemplo em python, simplesmente porque os geradores estão prontos para uso.


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

O ciclo se desdobra em algo assim (talvez não seja bem assim, mas o princípio é importante para nós):


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

Uma chamada para o generator() criará um iterador especial chamado gerador. A primeira chamada para next(gen) executa o código do início da função do generator até o primeiro yield , e o valor da variável local k de genertator() gravado na variável i . Cada próxima chamada para a next continuará executando a função com a instrução imediatamente após o yield anterior yield e assim por diante. Nesse caso, entre as next chamadas, os valores de todas as variáveis ​​locais dentro do generator são salvos.


É o mesmo, mas na língua Kotlin.


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

Em Java, poderíamos fazer uma geração preguiçosa mais ou menos assim:


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

Implementação 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 próximo lambda aninhado captura todas as variáveis ​​de todos os lambdas anteriores e é executado apenas quando o próximo elemento é solicitado. O resultado de cada lambda será o elemento gerado e o próximo bloco de código. Parece muito estranho, e duvido que alguém escreva assim. Denotamos o ideal pelo qual nos esforçaremos (e o alcançaremos, exceto que usaremos uma classe anônima em vez de lambdas).


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

A função passada para o construtor Sequence deve passar de yield para yield somente se necessário; os valores das variáveis ​​locais devem ser armazenados entre as chamadas para sequence.next() . Essa economia da pilha e o número da última instrução executada são chamados de preempção (o rendimento é traduzido para o russo) ou suspensão .


Continuação


Uma peça que pode ser espremida é chamada Continuação. A continuação é traduzida para o russo como 'continuação', mas chamarei de continuação. A Wikipedia escreve sobre continuação:


Continuação (Eng. Continuação) representa o estado do programa em um determinado momento, que pode ser salvo e usado para fazer a transição para esse estado. As continuações contêm todas as informações para continuar executando um programa a partir de um ponto específico.

Suponha que já tenhamos algum tipo de maneira mágica de implementar o mecanismo de continuações, que é representado pela seguinte interface. O método run pode parar sua execução. Cada chamada subsequente retoma a execução do último yield . Podemos pensar na continuação como um Runnable que pode ser executado em partes.


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

Usaremos a continuação assim:


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

E esperamos chegar a esta conclusão:


Saída
 Continuation start Next element: 1 Continuation resume Next element: 2 Continuation resume Next element: 3 Continuation end 

Sequence mediante solicitação do próximo elemento, chamará Continuation.run(scope) , que executará um bloco de código até o próximo rendimento e será excluído. A próxima chamada para Continuation.run(scope) começará o trabalho a partir do local da última exclusão e executará o código até o próximo yield . Sequence código de Sequence pode ser assim:


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

Tudo está bem, exceto que o java não é capaz de parar a execução do método em um local arbitrário, para que ele possa continuar a execução a partir do local da última parada. Portanto, teremos que fazer isso manualmente. Introduzimos o campo label, no qual armazenaremos o número do último rendimento chamado.


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

Temos uma máquina de estados (máquina de estados finitos) e, em geral, é exatamente isso que Kotlin faz em suas rotinas (você pode descompilar e ver se, é claro, entende alguma coisa). Temos quatro estados, cada chamada executada executa um pedaço de código e faz a transição para o próximo estado. Temos que salvar a variável local i no campo da classe. Além da complexidade injustificada, esse código tem outro problema: podemos passar valores diferentes como parâmetro de escopo para cada chamada de execução. Portanto, seria bom salvar o parâmetro de escopo no campo de classe na primeira chamada e continuar trabalhando com ele.


A continuação em java é implementada em nós, mas de uma maneira bastante estranha e em apenas uma instância. Cada vez que ninguém escreve algo semelhante, é difícil editar um código assim, é difícil ler esse código. Portanto, construiremos a máquina de estado após a compilação.


Suspenso e continuação


Como entendemos se a continuação concluiu o trabalho ou foi suspensa? Deixe o método run retornar um objeto SUSPEND especial em caso de suspensão.


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

Observe que eu removi o parâmetro de entrada da continuação. Devemos garantir que os parâmetros não mudem de chamada para chamada, a melhor maneira de fazer isso é excluí-los. O usuário, pelo contrário, precisa do parâmetro scope (ele será usado para muitas coisas, mas agora o SequenceScope é passado para seu lugar, do qual nosso yield é chamado). Além disso, o usuário não deseja saber sobre nenhum SUSPEND e não deseja retornar nada. Introduzir a interface Suspendable .


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

Por que uma classe abstrata, não uma interface?

Usar uma classe em vez de uma interface não permite escrever lambdas e força a criação de classes anônimas. Será muito conveniente trabalhar no bytecode com continuações como nas classes, porque os campos locais podem ser armazenados em seus campos. Mas lambdas no bytecode não se parecem com uma classe. Para detalhes, clique aqui .


Suspendable é Continuation em tempo de design, enquanto Continuation é Suspendable em Suspendable . O usuário grava o código no nível Suspendable e o código de baixo nível da biblioteca trabalha com Continuation . Ele se transforma em um após a modificação do bytecode.


Antes de falarmos sobre a prevenção após chamar o yield , mas no futuro precisaremos nos antecipar a outros métodos. @Suspend esses métodos com a anotação @Suspend . Isso se aplica ao yield si:


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

Lembre-se de que nossas continuações serão construídas com autômatos finitos. Vamos morar aqui em mais detalhes. É chamada de máquina de estados finitos porque possui um número finito de estados. Para armazenar o estado atual, usaremos um campo de rótulo especial. Inicialmente, rótulo é 0 - um estado zero (inicial). Cada chamada para Continuation.run executará algum tipo de código e entrará em algum estado (em qualquer outro que não seja o inicial). Após cada transição, a continuação deve salvar todas as variáveis ​​locais, o número do estado atual e executar o return SUSPEND . A transição para o estado final será denotada por return null (nos artigos a seguir retornaremos não apenas null ). Uma chamada para Continuation.run do estado final deve terminar com uma exceção ContinuationEndException .


Assim, o usuário escreve o código em Suspendable , após a compilação, ele se transforma em Continuation , com o qual a biblioteca trabalha e, em particular, em nossos geradores. A criação de um novo gerador para o usuário se parece com isso:


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

Mas o gerador em si precisa de continuação, porque ele precisa inicializar o campo Continuation<T> nextStep; . Para obter Continuation de Suspendable no código, escrevi uma aula 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); } } } 

Como essa mágica funciona? Com o parâmetro scope , o scope$S é inicializado via reflexão (o campo sintético que criaremos no bytecode). Uma continuação é inicializada apenas uma vez em createContinuation e uma segunda tentativa de inicialização resultará em uma execução. Em seguida, vem o tipo usual convertido para Continuation . Em geral, eu te enganei, toda a magia não está aqui. Como essa conversão de tipo é possível, o Suspendable específico passado já implementa Continuation . E isso aconteceu durante a compilação.


Estrutura do projeto


O projeto consistirá em três partes:


  • Código da biblioteca (API de nível baixo e alto)
  • Testes (de fato, somente neles agora você pode usar esta biblioteca)
  • Conversor suspenso -> continuação (implementado como tarefa gradle no gradle buildSrc)

Como o conversor está atualmente no buildSrc, será impossível usá-lo em algum lugar fora da própria biblioteca. Mas, por enquanto, não precisamos disso. No futuro, teremos duas opções: colocá-lo em um plug-in separado ou criar nosso próprio agente java (como o Quasar ) e realizar transformações em tempo de execução.


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 para Continuation será tratado pela tarefa TaskSuspendableTask. Não há nada de interessante na classe de tarefa de granizo, apenas seleciona as classes necessárias e as envia para conversão na classe SuspendableConverter . É ele quem agora nos interessa.


Geração de Bytecode


Para trabalhar com o bytecode, usaremos a biblioteca OW2 ASM. A biblioteca trabalha com o princípio do analisador SAX. Criamos um novo ClassReader, alimentamos uma classe compilada como uma matriz de bytes e chamamos o método accept(ClassVisitor visitor) . ClassReader irá analisar o bytecode e chamar os métodos apropriados no visitante passado ( visitMethod , visitClass , visitInsn ). O visitante pode trabalhar no modo adaptador e delegar chamadas para o próximo visitante. Normalmente, o último visitante é um ClassWriter , no qual o bytecode final é gerado. Se a tarefa não for linear (temos apenas uma), pode levar várias passagens pela classe. Outra abordagem fornecida pelo asm é gravar a classe em um ClassNode especial e fazer as transformações já nele. A primeira abordagem é mais rápida, mas pode não ser adequada para resolver problemas não lineares, então usei as duas abordagens.


Suspendable 3 classes Suspendable na conversão de Suspendable para Continuation :


  • SuspendInfoCollector - analisa o método Suspendable.run , coleta informações sobre todas as chamadas para os métodos @Suspend e sobre as variáveis ​​locais usadas.
  • SuspendableConverter - cria os campos obrigatórios, altera a assinatura e o identificador do método Suspendable.run para obter Continuation.run .
  • SuspendableMethodConverter - Converte o código do método Suspendable.run . Adiciona um código para salvar e restaurar variáveis ​​locais, salvando o estado atual no campo de label e passando para a instrução desejada.

Vamos descrever alguns pontos com mais detalhes.


A pesquisa para o método run parece com isso:


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

Espera-se que na classe conversível existam dois métodos de run , e um deles com o modificador de ponte (o que é lido aqui ). Estamos interessados ​​em um método sem modificador.


No bytecode da JVM, uma transição condicional (e incondicional) pode ser executada em qualquer lugar. O ASM possui uma abstração de Label especial (etiqueta), que é uma posição no bytecode. Ao longo do código, após cada chamada para os métodos @Suspend , colocaremos os rótulos nos quais faremos um salto condicional no início do método 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); // ,      } 

Colocamos marcadores após as chamadas dos 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); //   } 

Testes


Escrevemos um gerador que fornece três 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)); } } 

O teste em si não representa nada de interessante, mas é interessante o suficiente para descompilar o arquivo de classe.


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

O código está muito inchado, a maioria das instruções está salvando e restaurando o quadro de pilha (variáveis ​​locais). No entanto, ele funciona. O exemplo dado funcionaria perfeitamente sem a geração lenta. Vamos considerar um exemplo mais 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)); } } 

O código acima gera uma sequência infinita de Fibonacci. Nós compilamos e 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); } } 

Compreender o que torna uma classe descompilada bastante difícil. Como na última vez, a maioria das instruções direciona variáveis ​​locais para lá. Algumas das atribuições são inúteis e as variáveis ​​são imediatamente desgastadas por outros valores. , .


while, . . . , '' return SUSPEND .


Sumário


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


— — , . , . , : , .

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


All Articles