[ترجمة] متى تستخدم تيارات متوازية

المصدر
المؤلفون: دوغ ليا بالتعاون مع بريان جويتز ، بول ساندوز ، أليكسي شبيليف ، هاينز كابوتز ، جو بوير ، ...

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


ضع في اعتبارك استخدام S.parallelStream().operation(F) بدلاً من S.stream().operation(F) ، بشرط أن تكون العمليات مستقلة عن بعضها البعض وتكون إما مكلفة حسابياً أو يتم تطبيقها على عدد كبير من العناصر التي تم تقسيمها بشكل فعال (انشقاق) هياكل البيانات ، أو كليهما. بتعبير أدق:


  • F : وظيفة للعمل مع عنصر واحد ، عادة ما تكون لامدا ، مستقلة ، أي العملية على أي من العناصر مستقلة ولا تؤثر على العمليات على عناصر أخرى (للحصول على توصيات بشأن استخدام وظائف عديمة الحالة غير المتداخلة ، راجع وثائق حزمة الدفق ).
  • S : تم تقسيم المجموعة الأصلية بشكل فعال. بالإضافة إلى المجموعات ، هناك مجموعات أخرى مناسبة للتوازي ، وتدفق مصادر البيانات ، على سبيل المثال ، java.util.SplittableRandom (للتوازي مع ذلك يمكنك استخدام طريقة stream.parallel() ). ولكن تم تصميم معظم المصادر التي تحتوي على I / O في المركز بشكل أساسي للتشغيل المتسلسل.
  • يتجاوز إجمالي وقت التشغيل في الوضع التسلسلي الحد الأدنى المسموح به. اليوم ، بالنسبة لمعظم الأنظمة الأساسية ، فإن الحد يساوي تقريبًا (ضمن x10) 100 ميكروثانية. القياسات الدقيقة ، في هذه الحالة ، ليست مطلوبة. لأغراض عملية ، يكفي ببساطة مضاعفة N (عدد العناصر) في Q (وقت تشغيل واحد F ) ، ويمكن تقدير Q تقريبًا بعدد العمليات أو عدد أسطر التعليمات البرمجية. بعد ذلك ، تحتاج إلى التحقق من أن N * Q أقل من 10000 على الأقل (إذا كنت خجولًا ، أضف واحدًا أو اثنين من الأصفار). لذا ، إذا كانت F دالة صغيرة مثل x -> x + 1 ، فإن التنفيذ المتوازي سيكون منطقيًا عندما N >= 10000 . على العكس ، إذا كانت F عبارة عن حساب ثقيل ، على غرار العثور على أفضل حركة تالية في لعبة الشطرنج ، فإن قيمة Q كبيرة جدًا بحيث يمكن تجاهل N ، ولكن حتى يتم تقسيم المجموعة بالكامل.

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


  • البدء
    كان ظهور النوى الإضافية في المعالجات ، في معظم الحالات ، مصحوبًا بإضافة آلية إدارة الطاقة ، والتي يمكن أن تسبب تباطؤًا في إطلاق النواة ، وأحيانًا مع تراكبات إضافية من JVM ونظام التشغيل والمراقب المفرط. في هذه الحالة ، يتوافق الحد الذي يكون عنده الوضع الموازي منطقيًا تقريبًا مع الوقت المطلوب لبدء معالجة المهام الفرعية بعدد كافٍ من النوى. بعد ذلك ، يمكن أن تكون الحوسبة المتوازية أكثر كفاءة في استهلاك الطاقة من التتابع (اعتمادًا على تفاصيل المعالجات والأنظمة. للحصول على مثال ، انظر المقالة ).
  • تفصيل (حبيبات)
    نادرا ما يكون من المنطقي تقسيم الحسابات الصغيرة. عادة ما يقسم الإطار المهمة بحيث يمكن للأجزاء الفردية العمل على جميع نوى النظام المتاحة. إذا لم يكن هناك أي عمل تقريبًا لكل نواة بعد البداية ، فإن الجهود (المتسلسلة عادةً) لتنظيم الحوسبة المتوازية ستضيع. بالنظر إلى أن عدد النوى في الممارسة العملية يتراوح من 2 إلى 256 عتبة ، فإنه يمنع أيضًا التأثير غير المرغوب فيه للتقسيم المفرط للمهمة.
  • القسمة
    تتضمن المجموعات المقسمة بكفاءة أكثر ArrayList و {Concurrent}HashMap ، بالإضافة إلى المصفوفات العادية ( T[] ، التي يتم تقسيمها إلى أجزاء باستخدام طرق java.util.Arrays الثابتة). أقل LinkedList كفاءة هي LinkedList و BlockingQueue ومعظم المصادر التي تعتمد على I / O. والباقي في مكان ما في الوسط (عادة ما يتم تقسيم هياكل البيانات التي تدعم الوصول العشوائي و / أو البحث الفعال بكفاءة). إذا استغرق تقسيم البيانات وقتًا أطول من المعالجة ، فإن الجهد سدى. إذا كانت Q كبيرة بما يكفي ، فيمكنك الحصول على زيادة بسبب التوازي حتى مع LinkedList ، لكن هذه حالة نادرة إلى حد ما. بالإضافة إلى ذلك ، لا يمكن تقسيم بعض المصادر إلى عنصر واحد ، وبالتالي ، قد يكون هناك قيود على درجة تحلل المشكلة.

قد يكون من الصعب الحصول على الخصائص الدقيقة لهذه التأثيرات (على الرغم من ذلك ، إذا حاولت ، يمكن القيام بذلك باستخدام أدوات مثل JMH ). لكن من السهل ملاحظة التأثير التراكمي. لتشعر بنفسك - قم بتجربة. على سبيل المثال ، على جهاز اختبار 32 نواة ، عندما تقوم بتشغيل وظائف صغيرة ، مثل max() أو sum() ، فوق ArrayList نقطة التعادل تقارب 10000. لمزيد من العناصر ، لوحظ تسارع يصل إلى 20 مرة. ساعات العمل للمجموعات التي تحتوي على أقل من 10000 عنصر ليست أقل بكثير من 10000 ، وبالتالي فهي أبطأ من المعالجة المتسلسلة. تحدث أسوأ نتيجة بأقل من 100 عنصر - في هذه الحالة ، تتوقف الخيوط المعنية دون القيام بأي شيء مفيد ، لأن يتم الانتهاء من الحسابات قبل أن تبدأ. من ناحية أخرى ، عندما تستغرق العمليات على العناصر وقتًا طويلاً ، عند استخدام مجموعات قابلة للانقسام بكفاءة وبشكل كامل ، مثل ArrayList ، تظهر الفوائد على الفور.


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


سؤال وجواب


  • لماذا لا يستطيع JVM فهم موعد تنفيذ العمليات بالتوازي؟

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


  • ماذا لو كان لاتخاذ قرار جيد ليس لدي معرفة كافية بالمعلمات ( F ، N ، Q ، S

هذا أيضًا مشابه للمشكلات التي تتم مواجهتها في البرمجة التسلسلية. على سبيل المثال ، عادةً ما تعمل طريقة S.contains(x) لفئة Collection بسرعة إذا كانت S هي HashSet ، وبطيئة إذا كانت LinkedList ، LinkedList في حالات أخرى. عادة ، بالنسبة لمؤلف المكون الذي يستخدم المجموعة ، فإن أفضل طريقة للخروج من هذا الموقف هي تغليفه ونشر عملية محددة فقط عليه. ثم سيتم عزل المستخدمين عن الحاجة إلى الاختيار. الأمر نفسه ينطبق على العمليات المتوازية. على سبيل المثال ، يمكن لمكون لديه مجموعة أسعار داخلية تحديد طريقة تتحقق من حجمها إلى الحد الأقصى ، وهو أمر سيكون منطقيًا حتى تصبح الحوسبة أحادية البتات مكلفة للغاية. مثال:


 public long getMaxPrice() { return priceStream().max(); } private Stream priceStream() { return (prices.size() < MIN_PAR) ? prices.stream() : prices.parallelStream(); } 

يمكن توسيع هذه الفكرة إلى اعتبارات أخرى حول وقت وكيفية استخدام التزامن.


  • ماذا لو كانت وظيفتي من المحتمل أن تكون I / O أو عمليات متزامنة؟

من جهة أخرى ، هناك وظائف لا تفي بمعايير الاستقلال ، بما في ذلك عمليات الإدخال / الإخراج التسلسلية ، والوصول إلى حظر الموارد المتزامنة ، والحالات التي يؤثر فيها خطأ في مهمة فرعية موازية واحدة تؤدي مهام الإدخال / الإخراج على الآخرين. موازاةهما لا معنى لها. من ناحية أخرى ، هناك حسابات تقوم أحيانًا بإجراء إدخال / إخراج أو نادرًا ما يتم حظر المزامنة (على سبيل المثال ، معظم حالات التسجيل ، واستخدام مجموعات تنافسية مثل ConcurrentHashMap ). إنها غير ضارة. ما بينهما يتطلب المزيد من البحث. إذا كان من الممكن حظر كل مهمة فرعية لفترة طويلة في انتظار الإدخال / الإخراج أو الوصول ، فستكون موارد وحدة المعالجة المركزية خاملة دون إمكانية استخدامها بواسطة البرنامج أو JVM. من هذا أمر سيئ للجميع. في هذه الحالات ، لا تكون المعالجة المتدفقة المتوازية الخيار الصحيح دائمًا. ولكن هناك بدائل جيدة - على سبيل المثال ، I / O غير المتزامن ونهج CompletableFuture .


  • ماذا لو كان مصدري يعتمد على الإدخال / الإخراج؟

في الوقت الحالي ، باستخدام مولدات JDK Stream / I / O (على سبيل المثال ، BufferedReader.lines() ) ، يتم تكييفها بشكل أساسي للاستخدام في الوضع المتسلسل ، ومعالجة العناصر واحدًا تلو الآخر عندما تصبح متاحة. من الممكن دعم المعالجة المجمعة عالية الأداء للإدخال / الإخراج المخزنة ، ولكن في الوقت الحالي ، يتطلب ذلك تطوير مولدات خاصة Stream s و Spliterator s و Collector s. يمكن إضافة دعم بعض الحالات الشائعة في الإصدارات المستقبلية من JDK.


  • ماذا لو تم تشغيل برنامجي على جهاز كمبيوتر مشغول وكانت جميع النوى مشغولة؟

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


  • هل جميع العمليات متوازية عند استخدام الوضع الموازي؟

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


  • ما التسارع الذي سأحصل عليه من التزامن؟

إذا التزمت بهذه النصائح ، فعادة ما تكون كافية لفهمها. إن القدرة على التنبؤ ليست نقطة قوية للأجهزة والأنظمة الحديثة ، وبالتالي لا توجد إجابة عالمية. تعد بعض من العوامل التي لها تأثير كبير على موقع ذاكرة التخزين المؤقت وخصائص GC وتجميع JIT وتعارضات الوصول إلى الذاكرة وموقع البيانات وسياسات جدولة نظام التشغيل ووجود برنامج Hypervisor. يخضع أداء الوضع التتابعي أيضًا لتأثيرهم ، والذي عند استخدام التوازي ، غالبًا ما يتم تضخيمه: يمكن أن تؤدي المشكلة التي تسبب فرقًا بنسبة 10 في المائة في حالة التنفيذ المتسلسل إلى فرق 10 أضعاف في المعالجة المتوازية.


يتضمن إطار عمل الدفق بعض الميزات التي تساعد على زيادة فرص التسارع. على سبيل المثال ، استخدام التخصص IntStream ، مثل IntStream ، عادة ما يكون له تأثير أكبر على الوضع المتوازي من الوضع المتسلسل. والسبب هو أنه في هذه الحالة ، لا ينخفض ​​استهلاك الموارد (والذاكرة) فحسب ، بل يتحسن موقع ذاكرة التخزين المؤقت أيضًا. يؤدي استخدام ConcurrentHashMap بدلاً من HashMap ، في حالة التشغيل المتوازي لعملية collect ، إلى تقليل التكاليف الداخلية. ستظهر النصائح والحيل الجديدة كتجربة مكتسبة مع إطار العمل.


  • كل هذا مخيف للغاية! ألا يمكننا للتو وضع قواعد لاستخدام خصائص JVM لإيقاف التزامن؟

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

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


All Articles