DIY Coroutine. Bagian 1. Generator Malas

Di dunia JVM, coroutine terkenal berkat bahasa Kotlin dan Project Loom . Saya belum melihat deskripsi yang baik tentang prinsip coroutine Kotlinovsky, dan kode perpustakaan kotlin-coroutine sama sekali tidak bisa dipahami oleh orang yang tidak siap. Dalam pengalaman saya, kebanyakan orang hanya tahu tentang coroutine bahwa ini adalah "aliran ringan" dan bahwa di kotlin mereka bekerja melalui generasi bytecode yang cerdas. Jadi saya sampai baru-baru ini. Dan muncul ide kepada saya bahwa karena coroutine dapat diimplementasikan dalam bytecode, mengapa tidak mengimplementasikannya di java. Dari ide ini, selanjutnya, sebuah perpustakaan kecil dan cukup sederhana muncul, perangkat yang, saya harap, dapat dipahami oleh hampir semua pengembang. Detail di bawah potongan.



Kode sumber


Saya menyebut proyek Microutines, yang berasal dari kata Micro and Coroutines.


Semua kode tersedia di github . Awalnya, saya ingin membangun narasi sesuai dengan perkembangan pemahaman saya sendiri tentang topik, untuk berbicara tentang keputusan dan ide saya yang salah, tentang pengembangan api, tetapi kemudian kode dalam artikel akan sangat berbeda dari kode dengan github (yang tidak dapat dihindari dalam hal apapun, saya menulis satu artikel kali, dan pada komedi github dari waktu ke waktu). Oleh karena itu, saya sebagian besar akan menggambarkan versi final dari perpustakaan, dan jika beberapa bagian dari api tidak begitu jelas pada pandangan pertama, maka kemungkinan besar mereka diperlukan untuk menyelesaikan masalah yang akan kita bahas dalam artikel berikut.


Penafian


Ini adalah semacam proyek pelatihan. Saya akan senang jika salah satu pembaca cenderung untuk bermain atau bahkan mengajukan permintaan. Tetapi menggunakannya dalam produksi tidak sepadan. Cara terbaik untuk memahami teknologi adalah dengan mengimplementasikannya sendiri, yang merupakan satu-satunya tujuan proyek ini.


Dan saya tidak menjamin keakuratan istilah yang digunakan. Mungkin beberapa dari mereka saya dengar di suatu tempat, salah hafal, dan beberapa dari mereka bahkan muncul dengan diri saya sendiri dan lupa.


Apa itu coroutine?


Seperti yang telah saya perhatikan, sering dikatakan tentang coroutine bahwa ini adalah โ€œaliran yang difasilitasiโ€. Ini bukan definisi yang benar. Saya juga tidak akan memberikan definisi yang benar, tetapi saya akan mencoba menjelaskan apa itu coroutine. Memanggil coroutine flow tidak akan sepenuhnya benar. Coroutine adalah unit perencanaan yang lebih kecil dari stream, dan stream, pada gilirannya, lebih kecil dari unit penjadwalan. Perencanaan proses dan utas ditangani oleh sistem operasi. Corutin terlibat dalam perencanaan ... kita sendiri akan terlibat dalam perencanaan mereka. Coroutine bekerja di atas utas reguler, dan fitur utama mereka adalah mereka tidak memblokir utas saat mereka menunggu beberapa tugas lainnya selesai, tetapi melepaskannya untuk coroutine lain. Pendekatan ini disebut multitasking kooperatif. Corutin dapat bekerja pertama di satu utas, lalu di yang lain. Sebuah utas untuk coroutine bertindak sebagai sumber daya, dan sejuta coroutine dapat bekerja pada satu utas. Anda bisa melihat gambar ini:



Tugas kontrib 1 dan contrib 2 memenuhi beberapa jenis permintaan dan tidak menghalangi alur sambil menunggu tanggapan, tetapi menunda pekerjaan mereka dan melanjutkannya setelah menerima jawaban. Kita bisa menulis kode seperti itu menggunakan callback, katamu. Itu benar, tetapi inti dari coroutine adalah bahwa kita menulis kode tanpa panggilan balik, kita menulis kode berurutan biasa yang berjalan secara tidak sinkron.


Generator


Kami akan memulai pengembangan dari yang sederhana hingga yang kompleks. Hal pertama yang akan kita lakukan adalah generasi pengumpulan malas, diimplementasikan dalam beberapa bahasa menggunakan kata kunci hasil. Generator tidak disengaja di sini, seperti yang akan kita lihat nanti, dan generator dan coroutine dapat diimplementasikan menggunakan mekanisme yang sama.


Pertimbangkan contoh python, hanya karena generator ada di luar kotak.


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

Siklus terungkap menjadi sesuatu seperti ini (mungkin tidak seperti itu, tetapi prinsipnya penting bagi kami):


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

Panggilan ke generator() akan membuat iterator khusus yang disebut generator. Panggilan pertama ke next(gen) mengeksekusi kode dari awal fungsi generator ke yield pertama, dan nilai variabel lokal k dari genertator() ditulis ke variabel i . Setiap panggilan berikutnya ke next akan terus menjalankan fungsi dengan instruksi segera setelah yield sebelumnya yield dan seterusnya. Dalam hal ini, di antara panggilan next , nilai semua variabel lokal di dalam generator disimpan.


Itu hampir sama, tetapi dalam bahasa Kotlin.


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

Di Jawa, kita bisa melakukan generasi malas seperti ini:


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

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

Setiap lambda bersarang berikutnya menangkap semua variabel dari semua lambda sebelumnya dan dieksekusi hanya ketika elemen berikutnya diminta. Hasil dari setiap lambda akan menjadi elemen yang dihasilkan dan blok kode berikutnya. Terlihat sangat aneh, dan saya ragu ada orang yang mau menulis seperti itu. Kami menunjukkan ideal yang akan kami perjuangkan (dan kami akan mencapainya, kecuali bahwa kami akan menggunakan kelas anonim, bukan lambda).


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

Fungsi yang diteruskan ke konstruktor Sequence harus beralih dari yield ke yield hanya jika perlu, nilai-nilai variabel lokal harus disimpan di antara panggilan ke sequence.next() . Penghematan tumpukan ini dan jumlah instruksi yang terakhir dieksekusi disebut preemption (hasil diterjemahkan ke dalam bahasa Rusia) atau suspensi .


Lanjutan


Sepotong yang bisa diperas disebut Lanjutan. Kelanjutan diterjemahkan ke dalam bahasa Rusia sebagai 'kelanjutan', tetapi saya akan menyebutnya kelanjutan. Wikipedia menulis tentang kelanjutan:


Continuation (Eng. Continuation) mewakili keadaan program pada saat tertentu, yang dapat disimpan dan digunakan untuk transisi ke keadaan ini. Lanjutan berisi semua informasi untuk melanjutkan menjalankan program dari titik tertentu.

Misalkan kita sudah memiliki semacam cara ajaib yang menerapkan mekanisme kelanjutan, yang diwakili oleh antarmuka berikut. Metode run dapat menghentikan eksekusi. Setiap panggilan selanjutnya melanjutkan eksekusi dari yield terakhir. Kita dapat menganggap kelanjutan sebagai Runnable yang dapat dieksekusi di bagian-bagian.


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

Kami akan menggunakan kelanjutan seperti ini:


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

Dan kami berharap untuk mendapatkan kesimpulan ini:


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

Sequence atas permintaan elemen berikutnya akan memanggil Continuation.run(scope) , yang akan mengeksekusi blok kode sampai hasil berikutnya dan akan ramai keluar. Panggilan selanjutnya ke Continuation.run(scope) akan mulai bekerja dari tempat crowding out terakhir dan mengeksekusi kode sampai yield berikutnya. Kode Sequence bisa seperti ini:


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

Semuanya baik-baik saja, kecuali bahwa java tidak dapat menghentikan eksekusi metode di tempat yang sewenang-wenang, sehingga kemudian dapat melanjutkan eksekusi dari tempat pemberhentian terakhir. Karena itu, kita harus melakukan ini secara manual. Masukkan bidang label tempat kami akan menyimpan jumlah hasil yang disebut terakhir.


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

Kami memiliki mesin negara (mesin negara terbatas), dan pada umumnya inilah yang dilakukan Kotlin di coroutine-nya (Anda dapat mendekompilasi dan melihat apakah, tentu saja, Anda memahami sesuatu). Kami memiliki 4 negara bagian, setiap panggilan yang dijalankan mengeksekusi sepotong kode dan melakukan transisi ke keadaan berikutnya. Kita harus menyimpan variabel lokal i di bidang kelas. Selain kompleksitas yang tidak dapat dibenarkan, kode ini memiliki masalah lain: kita dapat memberikan nilai yang berbeda sebagai parameter lingkup untuk setiap panggilan yang dijalankan. Oleh karena itu, alangkah baiknya untuk menyimpan parameter lingkup di bidang kelas pada panggilan pertama, dan terus bekerja dengannya.


Kelanjutan pada java diimplementasikan pada kita, tetapi dengan cara yang agak aneh dan hanya dalam satu contoh. Setiap kali tidak ada yang akan menulis sesuatu yang serupa, mengedit kode seperti itu sulit, membaca kode seperti itu sulit. Oleh karena itu, kami akan membangun mesin negara setelah dikompilasi.


Dapat Ditangguhkan & Berlanjut


Bagaimana kita memahami jika kelanjutan telah menyelesaikan pekerjaan atau telah ditangguhkan? Biarkan metode run mengembalikan objek SUSPEND khusus jika terjadi penangguhan.


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

Perhatikan bahwa saya menghapus parameter input dari kelanjutan. Kita harus memastikan bahwa parameter tidak berubah dari panggilan ke panggilan, cara terbaik untuk melakukan ini adalah menghapusnya. Pengguna, sebaliknya, membutuhkan parameter scope (akan digunakan untuk banyak hal, tapi sekarang SequenceScope dilewatkan ke tempatnya, dari mana yield kami dipanggil). Selain itu, pengguna tidak ingin tahu tentang SUSPEND dan tidak ingin mengembalikan apa pun. Memperkenalkan antarmuka Suspendable .


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

Mengapa kelas abstrak, bukan antarmuka?

Menggunakan kelas sebagai ganti antarmuka tidak memungkinkan menulis lambdas dan memaksa menulis kelas anonim. Akan sangat nyaman bagi kita untuk bekerja dalam bytecode dengan kelanjutan seperti pada kelas, karena bidang lokal dapat disimpan di bidangnya. Tapi lambdas dalam bytecode tidak terlihat seperti kelas. Untuk detailnya, buka di sini .


Suspendable adalah Continuation dalam waktu desain, sedangkan Continuation adalah Suspendable dalam Suspendable . Pengguna menulis kode di tingkat Suspendable , dan kode tingkat rendah dari perpustakaan berfungsi dengan Continuation . Itu berubah menjadi satu setelah modifikasi bytecode.


Sebelum kita berbicara tentang preempting setelah memanggil yield , tetapi di masa depan kita perlu melakukan preempt setelah beberapa metode lain. Kami akan menandai metode tersebut dengan penjelasan @Suspend . Ini berlaku untuk yield sendiri:


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

Ingatlah bahwa kelanjutan kami akan dibangun di atas automata terbatas. Marilah kita tinggal di sini lebih terinci. Ini disebut mesin negara terbatas karena memiliki sejumlah negara terbatas. Untuk menyimpan status saat ini, kami akan menggunakan bidang label khusus. Awalnya, label adalah 0 - kondisi nol (awal). Setiap panggilan ke Continuation.run akan menjalankan beberapa jenis kode dan masuk ke beberapa kondisi (selain dari yang awal). Setelah setiap transisi, kelanjutan harus menyimpan semua variabel lokal, nomor negara saat ini, dan jalankan return SUSPEND . Transisi ke keadaan akhir akan dilambangkan dengan return null (dalam artikel berikut kami akan kembali tidak hanya null ). Panggilan ke Continuation.run dari keadaan akhir harus diakhiri dengan pengecualian ContinuationEndException .


Jadi, pengguna menulis kode di Suspendable , setelah kompilasi berubah menjadi Continuation , dengan mana perpustakaan bekerja, dan, khususnya, generator kami. Membuat generator baru untuk pengguna terlihat seperti ini:


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

Tetapi generator itu sendiri membutuhkan kelanjutan, karena ia perlu menginisialisasi bidang Continuation<T> nextStep; . Untuk mendapatkan Continuation dari Suspendable in code, saya menulis kelas Magic khusus.



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

Bagaimana cara kerja sihir ini? Dengan parameter scope , bidang scope$S diinisialisasi melalui refleksi (bidang sintetis yang akan kita buat dalam bytecode). Kelanjutan diinisialisasi hanya sekali di createContinuation , dan upaya inisialisasi kedua akan menghasilkan eksekusi. Berikutnya adalah tipe pemain biasa untuk Continuation . Secara umum, saya menipu Anda, semua keajaiban tidak ada di sini. Karena konversi jenis ini dimungkinkan, Suspendable spesifik yang diteruskan sudah menerapkan Continuation . Dan ini terjadi selama kompilasi.


Struktur proyek


Proyek ini akan terdiri dari tiga bagian:


  • Kode Perpustakaan (API Tingkat Rendah dan Tingkat Tinggi)
  • Tes (Bahkan, hanya di dalamnya sekarang Anda dapat menggunakan perpustakaan ini)
  • Konverter yang Dapat Ditangguhkan -> Lanjutan (Diimplementasikan sebagai tugas gradle dalam gradle buildSrc)

Karena konverter saat ini dalam buildSrc, tidak mungkin menggunakannya di luar perpustakaan itu sendiri. Tetapi untuk saat ini, kami tidak membutuhkannya. Di masa depan, kita akan memiliki dua opsi: menaruhnya di plugin terpisah, atau membuat agen java kita sendiri (seperti yang dilakukan Quasar ) dan melakukan transformasi dalam runtime.


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 hingga Continuation akan ditangani oleh tugas TaskSuspendableTask. Tidak ada yang menarik di kelas tugas hujan es, hanya memilih kelas yang diperlukan dan mengirimkannya untuk konversi ke kelas SuspendableConverter . Dialah yang sekarang menarik minat kita.


Generasi Bytecode


Untuk bekerja dengan bytecode, kita akan menggunakan pustaka OW2 ASM. Perpustakaan bekerja berdasarkan prinsip SAX parser. Kami membuat ClassReader baru, memberinya kelas yang dikompilasi sebagai array byte, dan memanggil metode accept(ClassVisitor visitor) . ClassReader akan mem-parse bytecode dan memanggil metode yang sesuai pada pengunjung yang berlalu ( visitMethod , visitClass , visitInsn ). Pengunjung dapat bekerja dalam mode adaptor dan mendelegasikan panggilan ke pengunjung berikutnya. Biasanya, pengunjung terakhir adalah ClassWriter , di mana bytecode terakhir dihasilkan. Jika tugasnya non-linear (kami hanya punya satu), mungkin diperlukan beberapa kali lewati kelas. Pendekatan lain yang disediakan oleh asm adalah menulis kelas ke ClassNode khusus, dan melakukan transformasi yang sudah ada di sana. Pendekatan pertama lebih cepat, tetapi mungkin tidak cocok untuk memecahkan masalah nonlinier, jadi saya menggunakan kedua pendekatan tersebut.


Suspendable 3 kelas yang Suspendable dalam konversi Suspendable ke Continuation :


  • SuspendInfoCollector - menganalisis metode Suspendable.run , mengumpulkan informasi tentang semua panggilan ke metode @Suspend dan tentang variabel lokal yang digunakan.
  • SuspendableConverter - membuat bidang yang diperlukan, mengubah tanda tangan dan pegangan metode Suspendable.run untuk mendapatkan Continuation.run .
  • SuspendableMethodConverter - Mengkonversi kode metode Suspendable.run . Menambahkan kode untuk menyimpan dan memulihkan variabel lokal, menyimpan keadaan saat ini di bidang label dan pindah ke instruksi yang diinginkan.

Mari kita jelaskan beberapa poin secara lebih rinci.


Pencarian untuk metode run terlihat seperti ini:


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

Diharapkan di kelas convertible akan ada dua metode yang run , dan salah satunya dengan pengubah jembatan (apa ini dibaca di sini ). Kami tertarik pada metode tanpa pengubah.


Dalam bytecode JVM, transisi bersyarat (dan tanpa syarat) dapat dilakukan di mana saja. ASM memiliki abstraksi Label khusus (label), yang merupakan posisi dalam bytecode. Sepanjang kode, setelah setiap panggilan ke metode @Suspend , kami akan menempatkan label yang akan kami buat lompatan bersyarat di awal metode 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); // ,      } 

Kami menempatkan label setelah panggilan metode @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); //   } 

Tes


Kami menulis generator yang memberikan tiga angka berturut-turut.


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

Tes itu sendiri tidak mewakili sesuatu yang menarik, tetapi cukup menarik untuk mendekompilasi file kelas.


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

Kode ini sangat membengkak, sebagian besar instruksi adalah menyimpan dan mengembalikan bingkai tumpukan (variabel lokal). Namun, itu berhasil. Contoh yang diberikan akan bekerja dengan sempurna tanpa generasi yang malas. Mari kita pertimbangkan contoh yang lebih sulit.


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

Kode di atas menghasilkan urutan Fibonacci yang tak terbatas. Kami mengkompilasi dan mendekompilasi:


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

Memahami apa yang membuat kelas yang didekompilasi cukup sulit. Seperti terakhir kali, sebagian besar instruksi mengarahkan variabel lokal di sana. Beberapa penugasan tidak berguna, dan variabel langsung terkoyak oleh nilai-nilai lain. , .


while, . . . , '' return SUSPEND .


Ringkasan


, , , . yield. , , โ€” , . , ( ) . , JIT . yield yieldAll โ€” , , , , . , , .


โ€” โ€” , . , . , : , .

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


All Articles