إطار عمل Executor
إطار عمل Executor
قبل الإصدار الخامس من Java، كان تشغيل شيء ما بالتزامن يعني إنشاء Thread يدويًا وتشغيله والأمل في النتائج. لهذا النهج مشكلتان جوهريتان: إنشاء الخيوط مكلف (يستهلك كل خيط نحو 1 ميغابايت من ذاكرة المكدس ومئات من الميكروثانية من وقت نظام التشغيل)، ولا توجد آلية مدمجة لإعادة الاستخدام أو الإبلاغ عن الأخطاء أو إدارة دورة الحياة. يُحل إطار عمل Executor — الذي أُدرج في java.util.concurrent في Java 5 — كل هذه المشكلات بفصل الماذا (وحدة العمل) عن الكيف (استراتيجية الخيوط التي تُنفّذه).
التجريد الجوهري: Executor
يرتكز الإطار بأكمله على واجهة واحدة:
هذه الطريقة الواحدة هي العقد كاملًا. أي كائن يقبل Runnable ويُشغّله في نهاية المطاف هو Executor. أبسط تطبيق ممكن يُشغّل المهمة مباشرةً على خيط الاستدعاء:
تطبيق أكثر فائدة يُدرج المهمة في قائمة انتظار على خيط جديد:
كلاهما تطبيقان صالحان لـ Executor. لا يتغير كود الاستدعاء — تتغير الاستراتيجية فقط. هذه هي قوة التجريد.
ExecutorService: إدارة دورة الحياة
يُوسّع ExecutorService واجهة Executor بإضافتين مهمتين: القدرة على إرسال مهام تُعيد نتائج، والقدرة على إيقاف تشغيل المُنفّذ بشكل منظم.
shutdown(). إن ExecutorService المُهملة هي سبب شائع لتوقف التطبيقات بدلًا من إنهائها نظيفًا.
مجمّعات الخيوط: لماذا توجد؟
مجمّع الخيوط هو ExecutorService يحتفظ بمجموعة ثابتة أو ديناميكية من خيوط العمل. عند إرسال مهمة، يُسلّمها المجمّع إلى خيط خامل (أو يُدرجها في القائمة إذا كانت جميع الخيوط مشغولة). عند انتهاء المهمة، يعود الخيط إلى المجمّع وينتظر المهمة التالية — دون تفكيك أو إعادة إنشاء.
الفوائد ملموسة:
- تقليل الكمون — الخيوط مُهيَّأة مسبقًا؛ إرسال مهمة يكاد لا يُكلّف شيئًا.
- التحكم في الموارد — تختار عدد الخيوط التي يمكن أن تعمل في وقت واحد، ما يمنع الانفجار الخيطي تحت الحِمل.
- إعادة الاستخدام — تتعامل كائنات الخيوط ذاتها مع آلاف المهام طوال عمرها.
إنشاء مجمّعات الخيوط باستخدام Executors
تُوفّر فئة المصنع Executors أكثر أنواع المجمّعات شيوعًا. إليك الأكثر استخدامًا:
مثال متكامل من البداية للنهاية: إرسال عشر مهام إلى مجمّع ثابت، ثم إيقاف تشغيله وانتظار الاكتمال.
شغّل هذا وسترى ثلاثة أسماء خيوط تتناوب على عشر مهام — دليل على إعادة استخدام الخيوط.
اختيار نوع المجمّع المناسب
- العمل المُقيَّد بالمعالج (CPU-bound) — استخدم
newFixedThreadPool(Runtime.getRuntime().availableProcessors()). خيوط أكثر من الأنوية تُضيف فقط تكاليف تبديل السياق دون زيادة الإنتاجية. - العمل المُقيَّد بالإدخال/الإخراج (IO-bound) — تقضي الخيوط معظم وقتها في الانتظار (شبكة، قرص). يُتيح المجمّع الأكبر للمعالج البقاء مشغولًا بينما تنتظر خيوط أخرى. منذ Java 21 يمكنك أيضًا استخدام الخيوط الافتراضية وهي أرخص بكثير للإدخال/الإخراج الحاجز.
- العمل غير المحدود مع حركة مرور متفجّرة —
newCachedThreadPool()مريح لكنه خطير تحت الحِمل المستدام: إذا وصلت المهام أسرع مما تُكتمل، ينشئ المجمّع خيوطًا غير محدودة وينفد الذاكرة. فضّلThreadPoolExecutorمخصصًا بطابور انتظار محدود في الإنتاج. - ضمان التسلسل — يمنحك
newSingleThreadExecutor()قناة تنفيذ متسلسلة مرتّبة دون أي مزامنة صريحة.
ThreadPoolExecutor كل هذه الخيارات. ستستكشفه بالتفصيل في الدرس القادم حول أنواع المجمّعات.
نمط الإيقاف الصحيح
يتبع الإيقاف القوي النمط ثنائي المرحلة الموصى به في توثيق Java:
shutdownNow() إشارة انقطاع لجميع الخيوط الجارية، وهذا يعمل فقط إذا كانت تلك الخيوط تتحقق فعلًا من حالة الانقطاع (مثل الحجب على إدخال/إخراج أو استدعاء Thread.sleep()). المهام التي تتجاهل الانقطاعات ستستمر في التشغيل بغض النظر.
الخلاصة
يُحل إطار عمل Executor محل إدارة الخيوط اليدوية بتجريد نظيف. يُفصل Executor إرسال المهام عن استراتيجية التنفيذ. يُضيف ExecutorService إدارة دورة الحياة وإرسال المهام التي تحمل نتائج. تُنشئ مجمّعات الخيوط الخيوط مسبقًا وتُعيد استخدامها لتقليل الكمون والتحكم في استهلاك الموارد. تُغطّي طرق المصنع في Executors أكثر حالات الاستخدام شيوعًا؛ اختر نوع المجمّع بناءً على ما إذا كانت مهامك مُقيَّدة بالمعالج أو بالإدخال/الإخراج. أوقف دائمًا خدمات المُنفّذ لمنع توقف JVM.