مشروع: معالج مهام متزامن
مشروع: معالج مهام متزامن
على مدار هذه الوحدة الدراسية بنيتَ صندوق أدوات كاملاً: مجمّعات الخيوط، وCallable وFuture، وCompletableFuture، والمجموعات المتزامنة، والأقفال، ومزامِنات التنسيق. في هذا المشروع الختامي ستجمع هذه القطع في تطبيق صغير لكنه واقعي — معالج مهام متزامن — يستقبل دُفعةً من عناصر العمل، ويعالجها بالتوازي عبر مجمّع خيوط محدود الحجم، ويجمع النتائج، ويُبلّغ عن التقدم فور اكتمال كل مهمة.
يتكوّن المشروع من ثلاث طبقات:
- تعريف المهمة — سجلّ يصف وحدة العمل الواحدة.
- المعالج — يُرسل المهام إلى
ExecutorService، ويجمع مقابضCompletableFuture، ويعالج النتائج المنتهية. - المُشغّل — يربط كل شيء معًا، ويُغذّي المهام، ويطبع ملخصًا نهائيًا.
الخطوة الأولى: نمذجة العمل
يجعل سجلّ Java تعريف المهمة موجزًا وغير قابل للتعديل. كل WorkItem له معرّف وحمولة وهمية:
equals وhashCode وtoString وحقولًا نهائية مجانًا. البيانات غير القابلة للتعديل التي تتدفق عبر الكود المتزامن دائمًا أأمن — لا حالة مشتركة قابلة للتعديل تحتاج إلى حماية.
الخطوة الثانية: بناء المعالج
تمتلك فئة المعالج مجمّع خيوط ثابت الحجم وتُعرّض طريقة واحدة تستقبل قائمة عناصر العمل وتُعيد قائمة من CompletableFuture<String>. استخدام CompletableFuture بدلًا من Future الخام يتيح ربط استدعاءات الاسترجاع دون إيقاف الخيط المُستدعي:
Thread.ofVirtual().factory() ينشئ مصنع خيوط افتراضية. تمريره إلى newFixedThreadPool يجعل كل مهمة تحصل على خيط افتراضي خفيف الوزن. للأحمال كثيفة الإدخال/الإخراج، يتوسّع هذا أفضل بكثير من خيوط المنصة — يُوقّف JVM الخيوط الافتراضية أثناء الاستدعاءات الحاجبة دون شغل خيط نظام تشغيل. للعمل الكثيف على المعالج، احتفظ بحجم المجمّع عند Runtime.getRuntime().availableProcessors().
الخطوة الثالثة: تجميع النتائج بعداد متزامن
مع اكتمال الـ futures نريد عدًّا حيًّا للنجاحات والإخفاقات. LongAdder مُصمَّم تحديدًا للزيادات عالية التنافس — أسرع من AtomicLong تحت ضغط الكتابة الشديد لأنه يشرّح العداد عبر خلايا:
الخطوة الرابعة: تشغيل خط المعالجة
ينشئ المُشغّل عناصر العمل، ويُرسلها، ويربط استدعاء استرجاع بكل عنصر لطباعة التقدم فور اكتمال كل future، ثم ينتظر الدُّفعة كاملة قبل طباعة الملخص:
thenAccept بدلًا من حلقة future.get()؟ استدعاء get() في حلقة يعالج النتائج بترتيب الإرسال — إذا استغرق العنصر 1 مدة 300 مللي ثانية لكن العنصر 2 انتهى في 50 مللي ثانية، تظل منتظرًا مع ذلك. thenAccept يُشغّل الاستدعاء على أي خيط يُكمل الـ future أولًا، فترى المخرجات تصل بترتيب الاكتمال لا الإرسال. ثم allOf(...).join() ينتظر فقط أبطأ عنصر.
المقايضات الرئيسية في التصميم
- حجم المجمّع مقابل شكل الحمل: للمهام كثيفة الإدخال/الإخراج (شبكة، قرص)، تتفوق الخيوط الافتراضية أو المجمّع الكبير على مجمّع ثابت صغير. للمهام كثيفة المعالج، مجمّع بحجم
availableProcessors()يُعظّم الإنتاجية دون الإفراط في الاشتراك. - عزل الأخطاء:
exceptionallyيحوّل الاستثناء إلى سلسلة علامة حتى لا يلغي عنصر معطوب واحد الدُّفعة كلها. بديل ذلكhandle()الذي يستقبل النتيجة والاستثناء معًا ويتيح إعادة كائن خطأ أغنى. - الضغط الخلفي: إذا أُنتجت المهام بسرعة تفوق معالجتها، تنمو قائمة انتظار المجمّع إلى ما لا نهاية. للأنظمة عالية الإنتاجية فكّر في
ThreadPoolExecutorمعArrayBlockingQueueمحدود وCallerRunsPolicy— هذا يُبطّئ المنتِج تلقائيًا. - قابلية المراقبة: في الإنتاج تستبدل
System.out.printlnبتسجيل منظّم وتُعرّض عدادات المُجمِّع عبر نقطة نهاية مقاييس (Micrometer، Prometheus).
try-with-resources. تنفيذ AutoCloseable يُغلق المجمّع عند خروج الكتلة. إذا أفلت منه مرجع، يمكن للمُستدعين إرسال مهام إلى منفّذ مُغلق والحصول على RejectedExecutionException في وقت التشغيل.
توسيع المشروع
بمجرد نجاح الخط الأساسي، فكّر في هذه التمارين لتعميق فهمك:
- مهلة لكل مهمة — لُفّ كل
supplyAsyncبـ.orTimeout(2, TimeUnit.SECONDS). المهام التي تستغرق وقتًا طويلًا تكتمل باستثناءTimeoutException. - قائمة انتظار ذات أولوية — استبدل القائمة بـ
PriorityBlockingQueue<WorkItem>مرتّبة بحقل أولوية. أطعم العناصر من القائمة إلىsubmitAllعلى دُفعات. - معالجة متعددة المراحل — اربط مرحلة
CompletableFutureثانية بعد الأولى لحفظ النتيجة في قاعدة بيانات. استخدمthenApplyAsyncمع منفّذ ثانٍ أصغر لتجنّب حجب مجمّع الحساب بعمليات الإدخال/الإخراج.
الخلاصة
جمع هذا المشروع كل مفهوم من الوحدة: ExecutorService للتنفيذ المُجمَّع، وCompletableFuture للتركيب غير الحاجب، وexceptionally لعزل الأخطاء، وLongAdder للتجميع عالي الأداء، وallOf لمزامنة الحاجز. الرؤية الجوهرية هي أن لكل أداة وظيفة — ادمجها بحسب الدور، واجعل الحالة المشتركة محدودة ومُحكمة الأنواع، ودع خط المعالجة الوظيفي يحمل البيانات عبر المراحل بنظافة.