Project Loom: مؤشرات ترابط Java الافتراضية قريبة

قبل بضعة أيام ، أنجب رون بريسلر مقالًا في State of Loom ، والذي لم يتمكن سوى عازف الجواهر الكسول من العثور عليه. المقالة جيدة حقًا ، هناك الكثير من الاستعارات المثيرة للاهتمام فيها ، والتي سأستخدمها الآن بدون ضمير الآن دون الرجوع إلى المصدر.

من ناحيتي ، سمحت لي عن غير قصد بالتعبير عن بعض الشك ، ولكن عندما يمكن العمل مع مشروع Loom في النهاية. حرفيا بعد ساعة ، جاء رد من رون نفسه - "وأنت تحاول!" . حسنا ، كان علي أن أحاول.

ما المطلوب للتجربة:


بادئ ذي بدء ، قررت النظر في الشكل الكلاسيكي للتدفقات من وثائق Kotlin Coroutine s. المثال مكتوب بلغة Kotlin ، لكن إعادة كتابته بلغة Java ليست صعبة:

public class Main {
    public static void main(String[] args) {
        var c = new AtomicLong();
        for (var i = 0; i < 1_000_000; i++) {
            new Thread(() -> {
                c.incrementAndGet();
            }).start();
        }

        System.out.println(c.get());
    }
}

نبدأ ونتأكد من أن المثال لا يزال معلقة ، كما كان من قبل:

javac Main.java && java Main

الآن أعد كتابة المثال باستخدام الخيوط الافتراضية التي يوفرها لنا Project Loom:

for (var i = 0; i < 1_000_000; i++) {
    Thread.startVirtualThread(() -> {
        c.incrementAndGet();
    });
}

والنتيجة ليست طويلة قادمة:

1000000

حتى متى؟
, .
-, Gil Tene, — .
-, Project Loom, . , — .

ولكن في حد ذاته لم يبهرني كثيرا. في النهاية ، مع coroutines في Kotlin ، يمكن تحقيق نفس النتيجة بسهولة.

يلاحظ رون في مقاله بشكل صحيح أنه كان على Kotlin تقديم وظيفة delay () ، التي تسمح لـ coroutine "بالنوم" ، لأن Thread.sleep () لا يرسل coroutine الحالي للنوم ، ولكن خيط جدولة الحالي ، الذي ليس كثيرًا ، عادة حسب الكمية وحدة المعالجة المركزية

ماذا عن Project Loom؟

for (var i = 0; i < 1_000_000; i++) {
  Thread.startVirtualThread(() -> {
    c.incrementAndGet();
    try {
        Thread.sleep(1_000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
  });
}

نتيجة:

- 400K

ولكن هذا مثير للاهتمام بالفعل! باستخدام Project Loom ، يمكن لاستدعاء Thread.sleep () أن يميز ما إذا كان في سلسلة رسائل عادية أو في سلسلة محادثات افتراضية ، ويعمل بشكل مختلف.

هذا في حد ذاته رائع جدا. ولكن دعنا نتعمق قليلاً:

var threads = new ArrayList<Thread>();
var cores = 10;
for (var i = 0; i < cores; i++) {
    var t = Thread.startVirtualThread(() -> {
        var bestUUID = "";
        for (var j = 0; j < 1_000_000; j++) {
            var currentUUID = UUID.randomUUID().toString();
            if (currentUUID.compareTo(bestUUID) > 0) {
                bestUUID = currentUUID;
            }
        }
        System.out.println("Best slow UUID is " + bestUUID);
    });
    threads.add(t);
}

for (var i = 0; i < cores; i++) {
    var t = Thread.startVirtualThread(() -> {
        var bestUUID = UUID.randomUUID().toString();
        System.out.println("Best fast UUID is " + bestUUID);
    });
    threads.add(t);
}

for (Thread t : threads) {
    t.join();
}

هنا ندير 10 مهام بطيئة و 10 مهام سريعة. المهام السريعة أسرع من المهام البطيئة مليون مرة ، لذلك سيكون من المنطقي افتراض أنها تكمل في وقت سابق.

لكن لم يكن هناك:

Best slow UUID is fffffde4-8c70-4ce6-97af-6a1779c206e1
Best slow UUID is ffffe33b-f884-4206-8e00-75bd78f6d3bd
Best slow UUID is fffffeb8-e972-4d2e-a1f8-6ff8aa640b70
Best fast UUID is e13a226a-d335-4d4d-81f5-55ddde69e554
Best fast UUID is ec99ed73-23b8-4ab7-b2ff-7942442a13a9
Best fast UUID is c0cbc46d-4a50-433c-95e7-84876a338212
Best fast UUID is c7672507-351f-4968-8cd2-2f74c754485c
Best fast UUID is d5ae642c-51ce-4b47-95db-abb6965d21c2
Best fast UUID is f2f942e3-f475-42b9-8f38-93d89f978578
Best fast UUID is 469691ee-da9c-4886-b26e-dd009c8753b8
Best fast UUID is 0ceb9554-a7e1-4e37-b477-064c1362c76e
Best fast UUID is 1924119e-1b30-4be9-8093-d5302b0eec5f
Best fast UUID is 94fe1afc-60aa-43ce-a294-f70f3011a424
Best slow UUID is fffffc24-28c5-49ac-8e30-091f1f9b2caf
Best slow UUID is fffff303-8ec1-4767-8643-44051b8276ca
Best slow UUID is ffffefcb-614f-48e0-827d-5e7d4dea1467
Best slow UUID is fffffed1-4348-456c-bc1d-b83e37d953df
Best slow UUID is fffff6d6-6250-4dfd-8d8d-10425640cc5a
Best slow UUID is ffffef57-c3c3-46f5-8ac0-6fad83f9d4d6
Best slow UUID is fffff79f-63a6-4cfa-9381-ee8959a8323d


يعمل الحدس فقط طالما أن عدد المهام البطيئة أقل من عدد مراكز وحدة المعالجة المركزية الخاصة بك.

والسبب بسيط - في الوقت الحالي يستخدم Project Loom ForkJoinPool المعتاد. نتيجة لذلك ، على الرغم من حقيقة أن الوثائق والفكرة تشير إلى أن التدفقات الافتراضية "استباقية" ، في الوقت الحالي تتصرف بطريقة "تعاونية". في الواقع مثل coroutines في Kotlin.

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

استدعاء دالة ، بخلاف Go ، لا ينتج عنه تبديل سياق. تعليق ، كما هو الحال في Kotlin ، لم يتم تسليمها أيضًا. ولكن يمكنك القيام به باستخدام Thread.yield () ، أو عن طريق استدعاء أي وظيفة Java IO: System.out.println ("") ، على سبيل المثال.

الحساب بسيط - معظم البرامج الحقيقية تستخدم حظر الإدخال / الإخراج في كثير من الأحيان. واستخدامه لمشروع Loom هو الذي يسعى للحل في المقام الأول.

بعض الاستنتاجات:


يجب أن أعترف أنه على الرغم من شكوكي الأولية ، فإن مشروع Loom قد أثار إعجابي بشكل إيجابي. بالنسبة لمستخدمي Java العاديين ، فهي تعد بقدرات تعدد مؤشرات خفيفة الوزن دون الحاجة إلى التبديل إلى لغة أخرى ، باستخدام مكتبة أو إطار عمل. والذي يبدو جيدًا بالفعل.

لكن الثورة الرئيسية ، كما أتوقع ، سيتم تنفيذ هذا المشروع بين مطوري المكتبات الذين لا يزال عليهم حل مشكلة التزامن مرارًا وتكرارًا. الآن ، مع انتشار JDK15 ، يمكن نقل هذه المشكلة إلى JVM ، حيث كانت تستخدم لنقل الذاكرة (GC) وتحسين الشفرة (JIT) إلى JVM.

رابط لمقالتي الأصلية إذا كنت تفضل القراءة باللغة الإنجليزية.

All Articles