Coroutines DIY. جزء 1. مولدات كسول

في عالم JVM ، تشتهر كوريوتن بفضل لغة Kotlin و Project Loom . لم أر وصفًا جيدًا لمبدأ كووتينلوفسكي ، ورمز مكتبة كوتلين كوروتيني غير مفهوم تمامًا لأي شخص غير مستعد. في تجربتي ، فإن معظم الناس يعرفون فقط عن coroutines أن هذه "تيارات خفيفة الوزن" وأنهم في kotlin يعملون من خلال توليد كود bytecode الذكي. لذلك كنت حتى وقت قريب. وقد جاءت الفكرة إليّ أنه نظرًا لأنه يمكن تنفيذ coroutines في رمز ثانوي ، فلماذا لا يتم تنفيذها في جافا. من هذه الفكرة ، ظهرت لاحقًا مكتبة صغيرة وبسيطة إلى حد ما ، وآمل أن يفهمها أي مطور تقريبًا. التفاصيل تحت خفض.



شفرة المصدر


دعوت مشروع Microutines ، الذي جاء من كلمتين Micro و Coroutines.


كل رمز متاح على جيثب . في البداية ، أردت بناء السرد وفقًا لتطور فهمي الخاص للموضوع ، للحديث عن قراراتي وأفكاري الخاطئة ، حول تطوير api ، ولكن بعد ذلك سيكون الرمز في المقالة مختلفًا تمامًا عن الكود مع github (وهو أمر لا مفر منه على أي حال ، أكتب مقال واحد مرات ، وعلى جيثب كوميدي من وقت لآخر). لذلك ، سوف أصف النسخة النهائية للمكتبة في الغالب ، وإذا كانت بعض أجزاء واجهة برمجة التطبيقات غير واضحة جدًا للوهلة الأولى ، فمن الأرجح أنها ضرورية لحل المشكلات التي سننظر فيها في المقالات التالية.


تنصل


هذا هو نوع من مشروع التدريب. سأكون سعيدًا إذا قام أحد القراء بإلحاحه على اللعب أو حتى تقديم طلب سحب. ولكن استخدامه في الإنتاج لا يستحق كل هذا العناء. أفضل طريقة لفهم التكنولوجيا بعمق هي تنفيذها بنفسك ، وهو الهدف الوحيد لهذا المشروع.


وأنا لا أضمن دقة المصطلحات المستخدمة. ربما سمعت بعضهم في مكان ما ، وحفظت بشكل غير صحيح ، والبعض منهم حتى جاء معي ونسي.


ما هي coroutines


كما لاحظت بالفعل ، كثيرا ما يقال عن coroutines أن هذه "تدفقات تسهيل". هذا ليس تعريف حقيقي. لن أعطي تعريفًا حقيقيًا أيضًا ، لكني سأحاول وصف ماهية هذه الأشياء ، coroutines. استدعاء التدفقات coroutines لن يكون صحيحا تماما. Coroutine هي وحدة تخطيط أصغر من الدفق ، والدفق ، بدوره ، أصغر من وحدة الجدولة. تتم معالجة عملية تخطيط مؤشر الترابط بواسطة نظام التشغيل. Corutin منخرطون في التخطيط ... نحن منخرطون في تخطيطهم. يعمل Coroutines أعلى سلاسل العمليات العادية ، وتتمثل الميزة الرئيسية في عدم حظر الخيط عندما ينتظرون إكمال مهمة أخرى ، ولكن حررها للحصول على coroutine آخر. ويسمى هذا النهج تعدد المهام التعاونية. Corutin يمكن أن تعمل أولا في موضوع واحد ، ثم في آخر. يعمل خيط coroutine كمورد ، ويمكن أن يعمل مليون coroutine على خيط واحد. تستطيع أن ترى هذه الصورة:



تفي مهام المساهمات 1 والمساهمة 2 بنوع من الطلبات ولا تمنع التدفق أثناء انتظار الردود ، ولكن توقف عملها وتواصله بعد تلقي الإجابات. يمكننا كتابة هذا الرمز باستخدام عمليات الاسترجاعات ، كما تقول. هذا صحيح ، ولكن جوهر coroutine هو أننا نكتب رمزًا دون عمليات رد الاتصال ، نكتب رمزًا تسلسليًا عاديًا يعمل بشكل غير متزامن.


مولدات


سنبدأ التطوير من البسيط الى المعقد. أول ما سنفعله هو إنشاء مجموعة كسول ، يتم تنفيذه في بعض اللغات باستخدام الكلمة المفتاحية. المولدات ليست مصادفة هنا ، كما سنرى لاحقًا ، ويمكن تنفيذ المولدات والمكورات باستخدام نفس الآلية.


دعنا نفكر في مثال في الثعبان ، ببساطة لأن المولدات موجودة خارج الصندوق.


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

تتكشف الدورة إلى شيء كهذا (ربما ليس هكذا ، ولكن المبدأ مهم بالنسبة لنا):


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

ستقوم استدعاء generator() بإنشاء مكرر خاص يسمى المولد. المكالمة الأولى إلى next(gen) تنفذ الرمز من بداية وظيفة generator إلى yield الأول ، genertator() كتابة قيمة المتغير المحلي k من genertator() إلى المتغير i . ستستمر كل مكالمة تالية إلى next في تنفيذ الوظيفة بالتعليمات التي تلي yield السابق مباشرةً yield وهكذا. في هذه الحالة ، بين المكالمات next ، يتم حفظ قيم جميع المتغيرات المحلية داخل generator .


هذا هو نفسه تقريبا ، ولكن في لغة Kotlin.


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

في Java ، يمكننا أن نفعل جيلًا كسولًا مثل هذا:


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

تنفيذ الدمية
 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(); } } } 

يلتقط كل lambda المتداخلة التالية جميع المتغيرات من جميع lambdas السابقة ويتم تنفيذه فقط عند طلب العنصر التالي. ستكون نتيجة كل لامدا هي العنصر الذي تم إنشاؤه وكتلة التعليمات البرمجية التالية. يبدو غريباً جداً ، وأشك في أن أي شخص سوف يكتب هكذا. نشير إلى المثل الأعلى الذي سنسعى إليه (وسنحققه ، باستثناء أننا سنستخدم فئة مجهولة بدلاً من lambdas).


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

يجب أن تنتقل الوظيفة التي تم تمريرها إلى مُنشئ التسلسل من yield إلى yield فقط إذا لزم الأمر ، يجب تخزين قيم المتغيرات المحلية بين المكالمات إلى sequence.next() . يسمى هذا الادخار للمكدس ورقم آخر تعليمة تم تنفيذها بـ preemption (يُترجم العائد إلى اللغة الروسية) أو التعليق .


Kontinuatsii


وتسمى القطعة التي يمكن ضغطها استمرار. تتم ترجمة استمرار إلى اللغة الروسية باسم "استمرار" ، ولكن سوف أسميها استمرار. ويكيبيديا تكتب عن الاستمرارية:


يمثل استمرار (المهندس استمرار) حالة البرنامج في لحظة معينة ، والتي يمكن حفظها واستخدامها للانتقال إلى هذه الحالة. تحتوي الاستمرارات على جميع المعلومات لمواصلة تنفيذ البرنامج من نقطة محددة.

لنفترض أن لدينا بالفعل طريقة سحرية تنفذ آلية الاستمرارية ، والتي تمثلها الواجهة التالية. طريقة run يمكن أن توقف تنفيذه. تستأنف كل مكالمة لاحقة التنفيذ من آخر yield . يمكننا التفكير في استمرار باعتباره Runnable التي يمكن تنفيذها في أجزاء.


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

سوف نستخدم استمرار مثل هذا:


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

ونتوقع الحصول على هذا الاستنتاج:


إنتاج
 Continuation start Next element: 1 Continuation resume Next element: 2 Continuation resume Next element: 3 Continuation end 

سوف يستدعي Sequence بناء على طلب العنصر التالي Continuation.run(scope) ، والذي سينفذ كتلة من الكود حتى الغلة التالية وتكون مزدحمة. سوف تبدأ المكالمة التالية إلى Continuation.run(scope) العمل من مكان آخر مزاحمة وتنفيذ الرمز حتى yield التالي. رمز Sequence يمكن أن يكون مثل هذا:


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

كل شيء على ما يرام ، باستثناء أن java غير قادر على إيقاف تنفيذ الطريقة في مكان تعسفي ، بحيث يمكن بعد ذلك الاستمرار في التنفيذ من مكان المحطة الأخيرة. لذلك ، سيتعين علينا القيام بذلك يدويًا. نعرض حقل التصنيف ، الذي سنخزن فيه عدد آخر عائد يسمى.


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

لدينا آلة حكومية (آلة حالة محدودة) ، وهذا إلى حد بعيد هو ما يفعله Kotlin في كوريناتيس (يمكنك إلغاء ترجمة ومعرفة ما إذا كنت بالطبع تفهم شيئًا ما). لدينا 4 حالات ، كل مكالمة قيد التنفيذ تنفذ جزءًا من الكود وتجري الانتقال إلى الحالة التالية. يتعين علينا حفظ المتغير المحلي i في حقل الفصل. بالإضافة إلى التعقيد غير المبرر ، يوجد لهذا الرمز مشكلة أخرى: يمكننا تمرير قيم مختلفة كمعلمة نطاق لكل مكالمة تشغيل. لذلك ، سيكون من الجيد حفظ معلمة النطاق في حقل الفصل على المكالمة الأولى ، ومواصلة العمل معها.


يتم تنفيذ استمرار java فينا ، ولكن بطريقة غريبة إلى حد ما وفي حالة واحدة فقط. في كل مرة لا يكتب أحد شيئًا مماثلاً ، يعد تحرير مثل هذا الرمز أمرًا صعبًا ، وقراءة مثل هذا الرمز أمر صعب. لذلك ، سوف نبني آلة الدولة بعد التجميع.


تعليق و استمرار


كيف نفهم ما إذا كان الاستمرار قد أكمل العمل أم توقف؟ اسمح لطريقة run بإرجاع كائن SUSPEND خاص في حالة التعليق.


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

لاحظ أنني أزلت معلمة الإدخال من الاستمرارية. يجب أن نتأكد من أن المعلمات لا تتغير من استدعاء إلى آخر ، فإن أفضل طريقة للقيام بذلك هي حذفها. على العكس من ذلك ، يحتاج المستخدم إلى معلمة scope (سيتم استخدامه لكثير من الأشياء ، ولكن الآن يتم تمرير SequenceScope إلى مكانه ، والذي يطلق عليه yield ). بالإضافة إلى ذلك ، لا يريد المستخدم معرفة أي SUSPEND ولا يريد إرجاع أي شيء. تقديم واجهة Suspendable .


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

لماذا فئة مجردة ، وليس واجهة؟

استخدام فئة بدلاً من واجهة لا يسمح بكتابة lambdas ويفرض كتابة فئات مجهولة. سيكون من المريح جدًا بالنسبة لنا العمل في رمز ثانوي مع استمرارية كما هو الحال مع الفصول ، لأنه يمكن تخزين الحقول المحلية في حقولهم. ولكن lambdas في bytecode لا تبدو وكأنها فئة. لمزيد من التفاصيل ، اذهب هنا .


Suspendable هي Continuation في وقت التصميم ، في حين Continuation Suspendable في Suspendable . يكتب المستخدم الكود في المستوى Suspendable ، ويعمل الكود المنخفض في المكتبة مع Continuation . إنه يتحول إلى واحد بعد تعديل الرمز الثانوي.


قبل أن نتحدث عن الاستباق بعد استدعاء yield ، ولكن في المستقبل سوف نحتاج إلى الاستباق بعد بعض الطرق الأخرى. سنقوم بتمييز هذه الأساليب مع تعليق توضيحي @Suspend . وهذا ينطبق على yield نفسه:


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

تذكر أنه سيتم بناء استمراراتنا على التشغيل الآلي المحدود. دعونا نتعمق هنا بمزيد من التفاصيل. يطلق عليه آلة الحالة المحدودة لأنه يحتوي على عدد محدود من الحالات. لتخزين الحالة الحالية ، سنستخدم حقل تسمية خاص. في البداية ، التسمية 0 - حالة الصفر (الأولي). كل مكالمة إلى Continuation.run ستنفذ نوعًا من التعليمات البرمجية وتذهب إلى حالة ما (في أي دولة أخرى غير الأولى). بعد كل عملية انتقال ، ينبغي أن يحفظ استمرار جميع المتغيرات المحلية ، ورقم الحالة الحالية وتنفيذ return SUSPEND . سيتم الإشارة إلى الانتقال إلى الحالة النهائية عن طريق return null (في المقالات التالية سنعود ليس فقط null ). يجب أن ينتهي استدعاء Continuation.run من الحالة النهائية مع استثناء ContinuationEndException .


لذلك ، يكتب المستخدم الكود في Suspendable ، بعد Suspendable البرمجي ، يتحول إلى Continuation ، والذي تعمل به المكتبة ، وخاصة المولدات الخاصة بنا. يبدو إنشاء مولد جديد للمستخدم كما يلي:


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

لكن المولد نفسه يحتاج إلى استمرار ، لأنه يحتاج إلى تهيئة Continuation<T> nextStep; . للحصول على Continuation من Suspendable في التعليمات البرمجية ، كتبت فئة 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); } } } 

كيف يعمل هذا السحر؟ باستخدام معلمة scope ، تتم تهيئة مجال scope$S عبر الانعكاس (الحقل التخليقي الذي سننشئه في الرمز الثانوي). تتم تهيئة استمرار مرة واحدة فقط في createContinuation ، وسوف تؤدي محاولة ثانية في التهيئة إلى تنفيذ. ويأتي بعد ذلك النوع المعتاد المدلى بها إلى Continuation . بشكل عام ، لقد خدعتك ، كل السحر ليس هنا. نظرًا لأن هذا النوع من التحويل ممكن ، فإن Suspendable المحددة Suspendable تنفيذها بالفعل Continuation . وحدث هذا أثناء التجميع.


هيكل المشروع


سيتألف المشروع من ثلاثة أجزاء:


  • رمز المكتبة (واجهة برمجة تطبيقات المستوى المنخفض والمستوى العالي)
  • الاختبارات (في الواقع ، فقط الآن يمكنك استخدام هذه المكتبة)
  • Converter Suspendable -> Continuation (يتم تنفيذه كمهمة مهمة في بناء gradle buildSrc)

نظرًا لأن المحول موجود حاليًا في buildSrc ، فسيكون من المستحيل استخدامه في مكان ما خارج المكتبة نفسها. لكن في الوقت الحالي ، نحن لسنا في حاجة إليها. في المستقبل ، سيكون لدينا خياران: جعله مكونًا إضافيًا منفصلاً أو إنشاء وكيل java خاص بنا (كما يفعل Quasar ) وإجراء تحويلات في وقت التشغيل.


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 إلى Continuation بواسطة المهمة TaskSuspendableTask. لا يوجد شيء مثير للاهتمام في فئة مهمة البرد ، فهو يختار فقط الفئات اللازمة ويرسلها للتحويل إلى فئة SuspendableConverter . هو الذي يهمنا الآن.


Bytecode الجيل


للعمل مع bytecode ، سوف نستخدم مكتبة ASM OW2. تعمل المكتبة على مبدأ محلل SAX. نقوم بإنشاء ClassReader جديد ، وإطعامها فئة مترجمة كصفيف من وحدات البايت ، واستدعاء طريقة accept(ClassVisitor visitor) . سيقوم ClassReader بتحليل bytecode واستدعاء الطرق المناسبة للزائر الذي تم تمريره ( visitMethod أو visitClass أو visitInsn ). يمكن للزائر العمل في وضع المحول وتفويض المكالمات للزائر التالي. عادةً ما يكون آخر زائر هو ClassWriter ، والذي يتم فيه إنشاء ClassWriter النهائي. إذا كانت المهمة غير خطية (لدينا واحدة فقط) ، فقد يستغرق الأمر عدة تمريرات خلال الفصل. هناك طريقة أخرى توفرها asm وهي كتابة الفصل الدراسي إلى ClassNode خاص ، وإجراء التحويلات بالفعل عليه. الطريقة الأولى أسرع ، ولكنها قد لا تكون مناسبة لحل المشكلات غير الخطية ، لذلك استخدمت كلتا الطريقتين.


Suspendable 3 فئات Suspendable في تحويل Suspendable to Continuation :


  • SuspendInfoCollector - يحلل أسلوب Suspendable.run ، ويجمع معلومات حول جميع المكالمات إلى أساليب @Suspend وحول المتغيرات المحلية المستخدمة.
  • SuspendableConverter - يقوم بإنشاء الحقول المطلوبة ، ويقوم بتغيير التوقيع والتعامل مع طريقة Suspendable.run للحصول على Continuation.run .
  • SuspendableMethodConverter - يحول رمز الأسلوب Suspendable.run . يضيف رمزًا لحفظ واستعادة المتغيرات المحلية ، وحفظ الحالة الحالية في حقل label والانتقال إلى التعليمات المطلوبة.

دعنا نصف بعض النقاط بمزيد من التفصيل.


يبدو البحث عن طريقة run كما يلي:


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

من المتوقع أنه في الفئة القابلة للتحويل سيكون هناك طريقتان run ، أحدهما باستخدام أداة تعديل الجسر (ما تتم قراءته هنا ). نحن مهتمون بطريقة دون تعديل.


في JVM bytecode ، يمكن إجراء انتقال مشروط (وغير مشروط) في أي مكان. يحتوي ASM على تجريد Label خاص (تسمية) ، وهو موضع في الرمز البريدي. من خلال التعليمات البرمجية ، بعد كل مكالمة إلى أساليب @Suspend ، @Suspend قفزة مشروطة في بداية طريقة 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 .


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

اختبارات


نكتب مولد يعطي ثلاثة أرقام على التوالي.


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

لا يمثل الاختبار نفسه أي شيء مثير للاهتمام ، ولكنه مثير للاهتمام بدرجة كافية لفك تشفير ملف الفصل.


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

الرمز منتفخ للغاية ، معظم التعليمات تقوم بحفظ واستعادة إطار المكدس (المتغيرات المحلية). ومع ذلك ، فإنه لا يعمل. المثال المعطى سوف يعمل بشكل مثالي بدون جيل كسول. دعنا نفكر في مثال أكثر صعوبة.


فيبوناكسي
 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)); } } 

الرمز أعلاه يولد تسلسل فيبوناتشي لانهائي. نحن نجمع ونفك الشفرة:


فيبوناتشي decompiled
 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); } } 

فهم ما يجعل الفصل decompiled صعبة بما فيه الكفاية. مثل آخر مرة ، تقود معظم الإرشادات المتغيرات المحلية هناك. بعض التعيينات غير مجدية ، والمتغيرات على الفور يتم تآكلها بواسطة قيم أخرى. , .


while, . . . , '' return SUSPEND .


ملخص


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


— — , . , . , : , .

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


All Articles