أنواع مجمّعات الخيوط
أنواع مجمّعات الخيوط
إنشاء Thread مستقل لكل وحدة عمل أمرٌ مكلف — فإنشاء الخيط يستلزم استدعاءات نواة النظام، وتخصيص الحزمة البرمجية (stack)، وحسابات الجدولة. تحل مجمّعات الخيوط (Thread Pools) هذه المشكلة بإبقاء مجموعة من الخيوط المُنشأة مسبقًا حيّة وإعادة استخدامها عبر مهام كثيرة. تمنحك فئة المصنع java.util.concurrent.Executors أربعة أنماط مجرَّبة من المجمّعات، كل منها مضبوط لنمط حمل عمل مختلف.
نظرة عامة على مصنع Executors
تعيد الطرق الأربع دومًا ExecutorService أو ScheduledExecutorService، فتتعامل دائمًا مع الواجهة نفسها بصرف النظر عن نوع المجمّع. الفارق كامن في عدد الخيوط واستراتيجية الطابور وكيفية التعامل مع ذروات الحمل.
مجمّع الخيوط الثابت
تُنشئ Executors.newFixedThreadPool(n) عددًا ثابتًا من n خيطًا وتبقيها حيّة للأبد (حتى تغلق المجمّع). تنتظر المهام المُرسَلة في طابور LinkedBlockingQueue غير محدود الحجم عندما تكون جميع الخيوط مشغولة.
مع 20 مهمة ومجمّع من 4 خيوط، تبدأ الدفعة الأولى من 4 مهام فورًا؛ وتنتظر المهام الـ16 المتبقية في الطابور وتُنجَز أربعة في كل مرة.
Runtime.getRuntime().availableProcessors() أو مضاعفات صغيرة منه). يمنع المجمّع الثابت الاستهلاك غير المحدود للموارد ويتجنّب عاصفة تبادل السياق الناجمة عن وجود خيوط قابلة للتشغيل أكثر بكثير من عدد الأنوية.
ThreadPoolExecutor مباشرةً مع ArrayBlockingQueue محدودة ونهج رفض (rejection policy).
مجمّع الخيوط المؤقّتة
تُنشئ Executors.newCachedThreadPool() خيوطًا جديدة عند الطلب وتُعيد استخدام الخيوط الخاملة. يُنهى الخيط الخامل لمدة 60 ثانية ويُزال من المجمّع، فيعود حجم المجمّع إلى الصفر حين يهدأ الحمل.
قد تصل إلى 100 خيط عند إرسال 100 مهمة قبل انتهاء أي منها. هذا مقبول للمهام القصيرة المُرتكزة على الإدخال/الإخراج — فالخيوط رخيصة الإبقاء لمدة 60 ثانية وتُعاد استخدامها بقوة.
المنفّذ ذو الخيط الواحد
تُنشئ Executors.newSingleThreadExecutor() مجمّعًا من خيط واحد بالضبط. تُنفَّذ المهام بالتسلسل وفق ترتيب الإرسال، مما يجعله أداة تسلسل مفيدة دون الحاجة إلى أي مزامنة يدوية.
newFixedThreadPool(1)، يُغلّف منفّذ الخيط الواحد عامله في مُفوِّض. إذا مات الخيط الداخلي بسبب استثناء غير مُعالَج، يُنشأ خيط جديد بصمت لاستبداله مع الإبقاء على المنفّذ حيًا. لا يُنشئ newFixedThreadPool(1) العادي خيطًا بديلًا.
مجمّع الخيوط المجدوَلة
تعيد Executors.newScheduledThreadPool(n) ScheduledExecutorService قادرًا على تشغيل المهام بعد تأخير أو وفق جدول دوري. إنه البديل الحديث لـ java.util.Timer.
هناك نوعان من التكرار: scheduleAtFixedRate يستهدف فترة ثابتة على ساعة الجدار (مثالي لنبضات القلب واستطلاع المقاييس)، بينما ينتظر scheduleWithFixedDelay انتهاء التشغيل السابق قبل بدء العدّ (مثالي حين تتباين مدة المهمة والتداخل خطر).
اختيار المجمّع المناسب
- مُكثِّف للمعالج، تزامن محدود —
newFixedThreadPool(cores) - مهام I/O قصيرة كثيرة، معدل متغيّر —
newCachedThreadPool() - طابور مهام تسلسلي دون مزامنة —
newSingleThreadExecutor() - تنفيذ مؤجَّل أو دوري —
newScheduledThreadPool(n)
الإغلاق الصحيح دائمًا
المنفّذ الذي لا يُغلَق أبدًا يُبقي خيوطه حيّة ويمنع الجهاز الافتراضي من الخروج ويسرب الموارد. استخدم نمط الإغلاق على مرحلتين:
System.exit() المباشر قد تُخفي المنفّذين المسرَّبين أثناء التطوير، لكن في الخدمة طويلة الأمد تتراكم بمرور الوقت وتستنفد ذاكرة مكدس الخيوط.
الخلاصة
يمنحك مصنع Executors أربعة أنواع جاهزة من المجمّعات. تُحدِّد المجمّعات الثابتة التزامن للمهام المُكثِّفة للمعالج؛ وتستوعب المجمّعات المؤقّتة طفرات الإدخال/الإخراج بمرونة؛ ويُسلسل منفّذ الخيط الواحد العمل؛ وتستبدل المجمّعات المجدوَلة Timer للمهام المؤجَّلة أو الدورية. أغلق المنفّذين دائمًا، وفي أحمال الإنتاج يُفضَّل بناء ThreadPoolExecutor مباشرةً حين تحتاج طابورًا محدودًا أو نهج رفض مخصصًا.