أدوات التزامن المتقدّمة

النتائج المستقبلية والحصول عليها

15 دقيقة الدرس 3 من 13

النتائج المستقبلية والحصول عليها

عندما تُرسل مهمّة إلى ExecutorService، تعود الاستدعاءة فورًا — لكنّ المهمّة تواصل تنفيذها في الخلفية. الجسر بين تلك العملية الخلفية وشيفرتك هو Future<V>: مقبض على حساب لم ينته بعد. إتقان كيفية استرجاع النتائج وتحديد مهلها وإلغائها أمر لا غنى عنه لكتابة برامج متزامنة صحيحة.

ما هو Future؟

java.util.concurrent.Future<V> واجهة جنيسة تحتوي خمس توابع. المعامل النوعي V هو نوع النتيجة عند اكتمال الحساب.

import java.util.concurrent.*; ExecutorService pool = Executors.newCachedThreadPool(); Future<Integer> future = pool.submit(() -> { // محاكاة عمل يستغرق وقتًا Thread.sleep(500); return 42; }); // --- يُكمل خيطك هنا بينما يعمل خيط التجمّع --- Integer result = future.get(); // يحجب حتى الاكتمال System.out.println("النتيجة: " + result); // النتيجة: 42 pool.shutdown();

يُعيد submit(Callable<V>) كائنًا من نوع Future<V>. تعمل Callable المُرسَلة على خيط التجمّع، بينما يحجب future.get() الخيط المُستدعي حتى تتوفّر النتيجة.

التوابع الخمسة لـ Future

  • get() — يحجب إلى الأبد حتى الاكتمال ثم يُعيد النتيجة.
  • get(long timeout, TimeUnit unit) — يحجب لمدة زمنية محدّدة؛ يُطلق TimeoutException عند انتهاء المهلة.
  • cancel(boolean mayInterruptIfRunning) — يحاول الإلغاء؛ يُعيد false إن كانت المهمّة انتهت بالفعل أو يتعذّر إلغاؤها.
  • isCancelled() — يُعيد true إن أُلغيت قبل اكتمالها.
  • isDone() — يُعيد true إن انتهت (طبيعيًا أو بالإلغاء أو باستثناء).

الحجب مع مهلة زمنية

الحجب إلى الأبد بـ get() خطأ شبه مؤكّد في التطبيقات الحقيقية — إذ قد تُعطّل مهمّة واحدة متوقّفة خيطك كلّه إلى الأبد. الأجدر الاستخدام النسخة ذات المهلة:

Future<String> future = pool.submit(() -> fetchFromRemoteService()); try { String value = future.get(2, TimeUnit.SECONDS); System.out.println("النتيجة: " + value); } catch (TimeoutException e) { System.err.println("الخدمة بطيئة جدًا — جارٍ الإلغاء"); future.cancel(true); // يُرسل إشارة إيقاف للخيط } catch (InterruptedException e) { Thread.currentThread().interrupt(); // استعادة راية الإيقاف } catch (ExecutionException e) { Throwable cause = e.getCause(); // الاستثناء الحقيقي من داخل المهمّة cause.printStackTrace(); }
تعامل دائمًا مع ExecutionException. إذا أطلقت Callable أي استثناء محدّد أو غير محدّد، يُغلّفه get() داخل ExecutionException. تجاهُله يعني ابتلاع الأخطاء في صمت. استدعِ دائمًا e.getCause() للحصول على المشكلة الحقيقية.

الإلغاء بالتفصيل

القيمة المنطقية المُمرَّرة لـ cancel() ذات أهمية:

  • cancel(false) — إذا لم تبدأ المهمّة بعد، يمنعها من البدء؛ وإن كانت تعمل بالفعل، يتركها تُكمل.
  • cancel(true) — علاوة على ذلك، يُرسل إشارة إيقاف للخيط الذي ينفّذ المهمّة.

الإيقاف يعمل فقط إذا تعاون الكود المُشغَّل — يجب أن يستدعي تابعًا حاجبًا يُطلق InterruptedException، أو يتحقّق دوريًا من Thread.currentThread().isInterrupted().

Future<Void> longTask = pool.submit(() -> { for (int i = 0; i < 1_000_000; i++) { if (Thread.currentThread().isInterrupted()) { System.out.println("أُوقف عند الخطوة " + i); return null; } doWork(i); } return null; }); Thread.sleep(100); longTask.cancel(true); // يُشير للخيط بالتوقّف
اكتب مهام قابلة للإيقاف. تحقّق دائمًا من راية الإيقاف داخل الحلقات الطويلة، وأعِد رفع InterruptedException أو استعِد الراية عند اصطيادها. المهام التي تتجاهل الإيقاف لا يمكن إلغاؤها بنظافة.

الاستثناءات المحدّدة من get()

يُعلن get() ثلاثة استثناءات محدّدة يجب معالجتها:

  • InterruptedException — أُوقف الخيط المنتظر نفسه. استعِد الراية دائمًا: Thread.currentThread().interrupt().
  • ExecutionException — أطلقت المهمّة استثناءً. افكّه بـ getCause().
  • TimeoutException (نسخة المهلة فقط) — انقضت المهلة قبل وصول النتيجة.

FutureTask: الجمع بين Runnable وFuture

تُنفّذ FutureTask<V> كلًّا من Runnable وFuture<V>. يمكنك تغليف Callable فيها وإرسالها للمنفّذ مع الاحتفاظ بمرجع Future مكتوب النوع — مفيد حين تحتاج لتمرير كائن المهمّة قبل إرساله:

FutureTask<Long> task = new FutureTask<>(() -> computeExpensiveValue()); new Thread(task).start(); // أو pool.execute(task) long result = task.get(); // يحجب حتى الاكتمال

الاستطلاع مقابل الحجب

يمكن تجنّب الحجب كليًّا إن كان لديك عمل آخر أثناء الانتظار:

Future<Data> f1 = pool.submit(() -> loadFromDatabase()); Future<Data> f2 = pool.submit(() -> loadFromNetwork()); // اعمل هنا على شيء محلّي ... Data db = f1.get(); // احجب فقط عند الحاجة الفعلية للنتيجة Data net = f2.get();

تعمل المهمّتان بالتوازي. لا تحجب إلا حين تحتاج النتائج فعلًا، لا عند إرسالها.

Future بسيط عمدًا — ومحدود عمدًا. لا يمكنك إرفاق استدعاء راجع، ولا تسلسل تحويلات، ولا دمج عدّة futures دون حجب الخيوط. هذا بالضبط ما دفع إلى تقديم CompletableFuture في Java 8. الدرسان التاليان يتناولانه بالتفصيل.

الخلاصة

يمنحك Future<V> مقبضًا على عملية حسابية غير متزامنة. استخدم نسخة المهلة من get() في كود الإنتاج، وتعامل دائمًا مع ExecutionException بفحص سببه، واستدعِ cancel(true) لإلغاء المهام المتفلّتة — لكن فقط إن كانت مهمّتك تتعاون مع الإيقاف. لتركيب أكثر ثراءً، الخطوة التالية هي CompletableFuture.