مهام Runnable و Callable
مهام Runnable و Callable
يفصل إطار Executor بين ما يجب فعله وبين كيفية وتوقيت تنفيذه. يُعبَّر عن "الماذا" عبر كائن مهمة — إما Runnable أو Callable. فهم أيّهما يناسب حالتك، ولماذا، هو أساس كتابة كود متزامن صحيح وقابل للصيانة.
Runnable: أطلق وانسَ
تتواجد java.lang.Runnable منذ الإصدار الأول من Java. عقدها بسيط للغاية: تابع وحيد run() لا يأخذ أي معاملات، يُعيد void، ولا يستطيع رمي استثناء محدود (checked exception).
execute(Runnable) هو تابع التسليم المقابل في Executor. يضع المهمة في الطابور ويعود فورًا — لا تحصل على أي مقبض، ولا طريقة لمعرفة وقت الانتهاء، ولا إمكانية لاسترداد نتيجة أو التقاط استثناء محدود يُرمى بالداخل.
run() لـ"تُعيدها"، فانتقل إلى Callable.
Callable: مهام تُعيد نتيجة
قُدِّمت java.util.concurrent.Callable<V> في Java 5 جنبًا إلى جنب مع إطار Executor. إنها واجهة وظيفية (functional interface) عامة بتابع وحيد: V call() throws Exception. الفارقان الجوهريان عن Runnable هما إعادة قيمة من نوع محدد وإمكانية رمي أي استثناء محدود.
يُدرج submit(Callable) المهمة في الطابور ويُعيد فورًا Future<V> — وعدًا بالنتيجة المقبلة. يمنع استدعاء future.get() الخيط الاستدعائي حتى تصبح النتيجة جاهزة. إذا رمت call() استثناءً فإن get() يُغلّفه في ExecutionException؛ استخرج السبب الأصلي عبر getCause().
تسليم Runnable عبر submit() مقابل execute()
يمكنك أيضًا تمرير Runnable إلى submit() — يُغلّفه التجمع ويُعيد Future<?> تُعيد get() منه null عند الاكتمال. يفيد هذا عندما تريد الانتظار حتى تنتهي مهمة جانبية دون الحاجة لنتيجة.
execute()، يُعدم أي استثناء غير محدود (unchecked) داخل المهمة بهدوء مخيف (يُغلق خيط العامل والتجمع يُعيد توليده، لكن تتبع المكدس يختفي ما لم تُنصّب UncaughtExceptionHandler). مع submit() يُخزَّن الاستثناء داخل Future ويُعاد رمؤه عند استدعاء get()، مما يُتيح لك التعامل معه.
تسليم عدة مهام Callable دفعة واحدة
يوفر ExecutorService طريقتين مريحتين للتسليم الجماعي:
invokeAll(Collection<Callable<T>>)— يُسلّم جميع المهام ويمنع حتى تكتمل كل واحدة (أو تنتهي المهلة الاختيارية). يُعيد قائمة منFutures جميعها في حالة اكتمال.invokeAny(Collection<Callable<T>>)— يُسلّم جميع المهام لكنه يُعيد نتيجة أولى مهمة تنجح ويُلغي الباقي. مفيد للحسابات الزائدة أو السباق بين مصادر بيانات متعددة.
انتشار الاستثناءات: فارق جوهري
هنا يُفاجأ كثير من المطورين في الإنتاج. مع Runnable المُسلَّم عبر execute()، ينتشر أي استثناء غير محدود إلى UncaughtExceptionHandler الخاص بالخيط — افتراضيًا يطبع تتبع المكدس فحسب ويُعوَّض الخيط. لا يُدرك الكود الاستدعائي بأي مشكلة.
مع Callable (أو Runnable مُسلَّم عبر submit())، يُخزَّن الاستثناء داخل Future. لا شيء يُطبع، لا شيء ينتشر. يظهر الاستثناء فقط عند استدعاء future.get():
submit() وأهملت الـ Future المُعادة دون استدعاء get() عليها يومًا ما، فإن أي استثناء داخل المهمة يُفقد بصمت تام. هذه واحدة من أكثر الأخطاء شيوعًا في كود Java المتزامن.
Callable مع مهلة زمنية
المنع اللانهائي على get() نادرًا ما يكون آمنًا في الإنتاج. الشكل المُحمَّل الزائد get(long timeout, TimeUnit unit) يرمي TimeoutException إن لم تنته المهمة في الوقت المحدد، مُتيحًا لك إلغاءها:
الاختيار بين Runnable و Callable: دليل القرار
- تحتاج قيمة مُعادة؟ ←
Callable - تحتاج نشر استثناء محدود بنظافة؟ ←
Callable - تحتاج معرفة متى تنتهي مهمة جانبية؟ ←
Runnableعبرsubmit() - أطلق وانسَ تمامًا بلا معالجة للأخطاء؟ ←
Runnableعبرexecute()(فقط إن كنت لا تكترث فعلًا بالإخفاقات) - تُركّب خطوط معالجة غير متزامنة؟ ←
CompletableFuture(مغطى في الدرس الخامس)
الخلاصة
Runnable هي مهمة بلا نتيجة ولا استثناء محدود. Callable<V> تضيف قيمة مُعادة من نوع محدد وشفافية كاملة للاستثناءات. سلّم المهام عبر execute() للتطاير والنسيان الحقيقي، أو عبر submit() للحصول على Future — وهو أمر بالغ الأهمية حين تحتاج نتائج أو انتظار الاكتمال أو معالجة موثوقة للأخطاء. استخدم invokeAll لتوزيع دفعة من Callables وجمع جميع النتائج، أو invokeAny للتسابق بينها. احرص دائمًا على معالجة الـ Future التي تحصل عليها من submit()؛ تجاهلها يبتلع الاستثناءات بصمت.