اذهب إلى المحتوى

البرمجة باستخدام الخيوط threads في جافا


رضوى العربي

تضيف الخيوط threads مستوًى جديدًا من التعقيد إلى البرمجة، ولكنها مهمةٌ وستصبح أساسيةً بالمستقبل، ولذلك لا بُدّ أن يَطلِّع كل مبرمجٍ على بعض أنماط التصميم design pattern الأساسية المُستخدَمة مع الخيوط، حيث سنفحص بهذا المقال بعض التقنيات البسيطة وسنبني عليها بالأقسام التالية.

الخيوط والمؤقتات ومكتبة جافا إف إكس

يُمكِننا استخدام الخيوط لتنفيذ مهمةٍ معينةٍ تنفيذًا دوريًا، وهو ما يُعدّ أمرًا بسيطًا لدرجة وجود أصنافٍ مُتخصِّصة لتنفيذ تلك المهمة، ولقد تعاملنا مع إحداها بالفعل، وهو الصنف AnimationTimer المُعرَّف بحزمة javafx.animation، التي درسناها بالقسم الفرعي الصنف AnimationTimer من المقال تعرف على أهم الأحداث والتعامل معها في مكتبة جافا إف إكس JavaFX حيث يستدعِي ذلك الصنف تابعه handle()‎ دوريًا بمعدل 60 مرةٍ لكل ثانية. في الواقع، كان اِستخدَام الخيوط ضروريًا لتنفيذ العمليات المشابهة قبل أن تتوفَّر المؤقتات.

لنفترض أننا نريد فعل شيءٍ مشابه باستخدام خيط، كأن نَستدعِي برنامجًا فرعيًا subroutine على فتراتٍ دورية، مثل 30 مرةٍ لكل ثانية. سيُنفِّذ تابع الخيط run()‎ حلقة تكرار loop، سيتوقَّف خلالها الخيط لمدة "30 ميللي ثانية"، ثم سيستدعِي بعدها البرنامج الفرعي. تُنفِّذ الشيفرة التالية ذلك باستخدِام Thread.sleep()‎ -الذي ناقشناه بقسم العمليات على الخيوط من المقال السابق مقدمة إلى الخيوط Threads في جافا- ضمن صنفٍ متداخل nested:

private class Animator extends Thread {
    public void run() {
       while (true) {
           try {
               Thread.sleep(30);
           }
           catch (InterruptedException e) {
           }
           callSubroutine();
       }
    }
}

سنُنشِئ الآن كائنًا ينتمي إلى ذلك الصنف، ونَستدعِي تابعه start()‎، مع الملاحظة بأنه لن يكون هناك أي طريقةٍ لإيقاف الخيط بعد تشغيله؛ وإنما يُمكِننا إيقاف حلقة التكرار عندما تساوي قيمة متغيرٍ متطايرٍ volatile منطقي معين وليكن اسمه هو terminate القيمة true كما ناقشنا بقسم المتغيرات المتطايرة من المقال السابق. إذا أردنا تشغيل التحريكة animation مرةً أخرى بعد إيقافها، فسنضطّر لإنشاء كائنٍ جديدٍ من النوع Thread؛ نظرًا لأنه من الممكن تنفيذ تلك الكائنات مرةً واحدةً فقط. سنناقش بالقسم التالي بعض التقنيات المُستخدَمة للتحكم بالخيوط.

تختلف الخيوط عن المؤقتات جزئيًا فيما يتعلّق بالتحريكات؛ حيث لا يفعل الخيط الذي يَستخدِمه الصنف AniamtionTimer والمُعرَّف بمكتبة جافا إف إكس أكثر من مجرد استدعاء البرنامج handle()‎ مرةً بعد أخرى، والذي يُنفَّذ ضمن خيط تطبيق جافا إف إكس المسؤول عن إعادة رسم مكوِّنات الواجهة والإستجابة لما يفعله المُستخدِم. يُعدّ ذلك أمرًا مهمًا لأن مكتبة جافا إف إكس ليست آمنةً خيطيًا thread-safe؛ بمعنى أنها لا تَستخدِم المزامنة synchronization لتجنُّب حالات التسابق race conditions الممكن حدوثها بين الخيوط التي تحاول الوصول إلى كلٍ من مكوِّنات واجهة المُستخدِم الرسومية GUI ومتغيرات الحالة الخاصة بها. لا يُمثِّل ذلك مشكلةً بشرط أن يحدث كل شيء ضمن خيط التطبيق.

في المقابل، قد تَنشَأ مشكلةٌ إذا حاول خيطٌ آخر تعديل إحدى مُكوِّنات الواجهة أو إحدى المتغيرات المُستخدَمة بخيط واجهة المُستخدِم الرسومية، وعندها قد يكون اِستخدَام المزامنة حلًا مناسبًا مع أن اِستخدَام الصنف AnimationTimer -إن كان ذلك ممكنًا- عادةً ما يكون الحل الأمثل؛ ولكن يُمكِنك استخدام Platform.runLater()‎، إذا كنت مضطّرًا لاستخدام خيطٍ منفصل.

تحتوي حزمة javafx.application على الصنف Platform الذي يتضمَّن التابع الساكن Platform.runLater(r)‎؛ حيث يَستقبِل هذا التابع كائنًا من النوع Runnable، أي نفس الواجهة interface المُستخدَمة لإنشاء الخيوط على أنه معاملٌ بإمكاننا اِستدعائه من أي خيط. تتلّخص مسؤولية ذلك التابع في تسليم r إلى خيط تطبيق جافا إف إكس لتنفيذه، ثم يعود مباشرةً دون أن ينتظر انتهاء تنفيذ r. بعد ذلك، يَستدعِي خيط التطبيق التابع r.run()‎ بعد أجزاءٍ من الثانية أو حتى على الفور إذا لم يَكُن الحاسوب مُنشغِّلًا بتنفيذ شيءٍ آخر.

تُنفَّذ الأصناف المُنفِّذة للواجهة Runnable بنفس ترتيب تَسلُمها، ونظرًا لأنها تُنفَّذ داخل خيط التطبيق، يُمكِنها معالجة واجهة المُستخدِم الرسومية معالجةً آمنةً بدون مزامنة. يُمرَّر معامل التابع Platform.runLater()‎ عادةً مثل تعبير لامدا lambda expression من النوع Runnable. سنَستخدِم Platform.runLater()‎ بعدة أمثلة خلال هذا المقال وما يليه.

سنفحص الآن المثال التوضيحي RandomArtWithThreads.java الذي يَستخدِم خيطًا للتحكُّم بتحريكةٍ بسيطةٍ جدًا. لا يفعل الخيط بهذا المثال أكثر من مجرد استدعاء التابع redraw()‎ كل ثانيتين، الذي يُعيد رسم محتويات الحاوية canvas؛ واستخدام التابع Platform.runLater()‎ لتنفيذ redraw()‎ ضمن خيط التطبيق. يستطيع المُستخدِم الضغط على زر لبدء التحريكة وإيقافها. يُنشَأ خيطٌ جديدٌ بكل مرة تبدأ خلالها التحريكة، ويُضبَط متغيرٌ منطقيٌ متطايرٌ اسمه running إلى القيمة false عندما يُوقِف المُستخدِم التحريكة إشارةً للخيط بأن عليه أن يتوقف، كما ناقشنا بالمقال المتغيرات المتطايرة من المقال السابق. يُعرِّف الخيط بواسطة الصنف التالي:

private class Runner extends Thread {
    public void run() {
        while (running) {
            Platform.runLater( () -> redraw() );
            try {
                Thread.sleep(2000);  // انتظر ثانيتين قبل إعادة رسم الشاشة
            }
            catch (InterruptedException e) {
            }
        }
    }
}

التعاود داخل الخيوط

إذا كان الخيط يُنفِّذ خوارزميةً تعاوديةً recursive (ناقشنا التعاود في مقال التعاود recursion في جافا)، وكنت تريد إعادة رسم الواجهة عدة مرات أثناء حدوث التعاود؛ فقد تضطّر لاستخدام خيطٍ منفصلٍ للتحكُّم بالتحريكة. من الصعب تقسيم خوارزمية تعاودية إلى سلسلةٍ من استدعاءات التوابع داخل مؤقت، فمن البديهي أكثر استدعاء تابعٍ تعاودي واحدٍ لإجراء التعاود، وهو أمرٌ سهل إنجازه ضمن خيط.

سنفحص المثال التوضيحي QuicksortThreadDemo.java الذي يَرسِم تحريكةً تُوضِح طريقة عمل خوارزمية QuickSort التعاودية لترتيب المصفوفات. ستحتوي المصفوفة في هذا المثال على ألوان، وسيكون الهدف هو ترتيبها وفقًا لسلّم الألوان المعروف من الأحمر إلى البنفسجي. يَسمَح البرنامج للمُستخدِم أيضًا بالنقر على زر "Start" لبدء العملية، وعندها تُرتَّب الألوان ترتيبًا عشوائيًا، ثم تُستدعَى خوارزمية QuickSort لترتيبها وتُعرَض العملية بحركة بطيئة. يتبدَّل زر "Start" أثناء عملية الترتيب إلى "Finish" ليَسمَح للمُستخدِم بإيقاف عملية الترتيب قبل انتهائها. في الواقع، من الممتع مشاهدة خرج هذا البرنامج، ولربما يساعدك حتى على فهم طريقة عمل خوارزمية QuickSort على نحوٍ أفضل، لذلك عليك أن تُجرِّب تشغيله.

ينبغي أن تتغيّر الصورة المعروضة بالحاوية في هذا البرنامج بكل مرةٍ تُجرِي خلالها الخوارزمية تعديلًا على المصفوفة. لاحِظ أن المصفوفة تتغيّر بخيط التحريكة بينما لا بُدّ من إجراء التغيير المقابل على الحاوية بخيط تطبيق جافا إف إكس باستخدام Platform.runLater()‎ كما ناقشنا بالأعلى. بكل مرة يُستدعى خلالها Platform.runLater()‎، يتوقف الخيط لمدة "100 ميللي ثانية" ليَسمَح لخيط التطبيق بتنفيذ المعامل المُمرَّر من النوع Runnable وليتمكَّن المُستخدِم من مشاهدة التعديلات. هناك أيضًا توقُّفٌ أطول بما يصِل إلى ثانيةٍ كاملة بعد ترتيب عناصر المصفوفة عشوائيًا مباشرةً وقبل بدء عملية الترتيب الفعلي. يُعرِّف الصنف QuicksortThreadDemo التابع delay()‎ الذي يجعل الخيط المُستدِعي له يتوقَّف لفترة معينة، نظرًا لأن الشيفرة تتوقَّف بأكثر من مكان.

والآن، كيف نُنفِّذ شيفرة الزر "Finish" المسؤولة عن إيقاف عملية الترتيب وإنهاء الخيط؟ في الواقع، يؤدي النقر على هذا الزر إلى ضبط قيمة المتغير المنطقي المتطاير running إلى القيمة false إشارةً للخيط بأنه عليه الانتهاء. تَكْمُن المشكلة في إمكانية النقر عليه بأي لحظة، حتى لو كان البرنامج منهمكًا بتنفيذ الخوارزمية وبمستوًى منخفضٍ جدًا من التعاود.

لا بُدّ أن تعود جميع استدعاءات التوابع التعاودية لنتمكّن من إنهاء الخيط، ويُعد التبليغ عن استثناء exception إحدى أبسط الطرق التي يُمكنِها تحقيق ذلك. يُعرِّف الصنف QuickSortThreadDemo صنف استثناءٍ جديد اسمه ThreadTerminationException لهذا الغرض، ويفحص التابع delay()‎ قيمة المُتغيّر running؛ فإذا كانت مساويةً للقيمة false، سيُبلِّغ عن استثناءٍ تسبَّب بإنهاء الخوارزمية التعاودية، وبالتتابع خيط التحريكة ذاته. ألقِ نظرةً على تعريف التابع delay()‎:

private void delay(int millis) {
   if (! running)
      throw new ThreadTerminationException();
   try {
      Thread.sleep(millis);
   }
   catch (InterruptedException e) {
   }
   if (! running) // افحصها مرة أخرى فربما تكون قد تغيرت أثناء توقُّف الخيط
      throw new ThreadTerminationException();
}

يَلتقِط تابع الخيط run()‎ الاستثناء المنتمي للصنف ThreadTerminationException:

// 1
private class Runner extends Thread {
    public void run() {
        for (int i = 0; i < hue.length; i++) {
               // املأ المصفوفة باستخدام الفهارس
            hue[i] = i;
        }
        for (int i = hue.length-1; i > 0; i--) { 
               // رتّب المصفوفة عشوائيًا
            int r = (int)((i+1)*Math.random());
            int temp = hue[r];
            hue[r] = hue[i];
               // 2
            setHue(i,temp);
        }
        try {
            delay(1000);  // انتظر ثانية قبل بدء عملية الترتيب
            quickSort(0,hue.length-1);  // رتّب المصفوفة بالكامل
        }
        catch (ThreadTerminationException e) { // ألغى المُستخدِم عملية الترتيب
                // 3
            Platform.runLater( () -> drawSorted() );
        }
        finally {
            running = false;  // 4
            Platform.runLater( () -> startButton.setText("Start") );
        }
    }
}

حيث أن:

  • [1]: يُعرِّف هذا الصنف خيطًا يُنفِّذ خوارزمية QuickSort التعاودية؛ حيث يبدأ الخيط بخلط عناصر المصفوفة hue عشوائيًا، ثم يَستدعِي التابع quickSort()‎ لترتيبها بالكامل. إذا توقَّف التابع quickSort نتيجة استثناء من النوع ThreadTerminationException -يحدُث إذا نقر المُستخدِم على زر "Finish"-، يُعيد الخيط المصفوفة إلى حالتها المُرتَّبة قبل أن ينتهي؛ وبالتالي سواءٌ ألغى المُستخدِم عملية الترتيب أم لا، تكون المصفوفة مرتبةً بنهاية الخيط. في جميع الحالات، يُضبَط نص الزر إلى "Start" بالنهاية.
  • [2]: التعليمة الأخيرة التي ينبغي إنجازها ضمن الحلقة هي hue = temp. لن تتغير قيمة hue بعد ذلك، ولذلك تُنجز عملية الإسناد باستدعاء التابع setHue(i,temp)‎ الذي سيُبدِّل القيمة الموجودة بالمصفوفة، كما أنه يَستِخدِم Platform.runLater()‎ لتغيير لون الشريط رقم i بالحاوية.
  • [3]: ضع الألوان بصورةٍ مرتبة. يرسم التابع drawSorted()‎ ألوان جميع الشرائط بالترتيب.
  • [4]: تأكَّد من أن running يُساوِي false. يكون ذلك ضروريًا فقط إذا انتهى الخيط طبيعيًا.

يَستخدِم البرنامج المتغير runner من النوع Runner لتمثيل الخيط المسؤول عن عملية الترتيب. عندما ينقر المُستخدِم على الزر "Start"، تُنفَّذ الشيفرة التالية لإنشاء الخيط وتشغيله:

startButton.setText("Finish");
runner = new Runner();
running = true;  // اضبط قيمة الإشارة قبل تشغيل الخيط
runner.start();

لا بُدّ من ضبط قيمة متغير الإشارة running إلى القيمة true قبل بدء الخيط؛ لأنه لو كان يحتوي على القيمة false بالفعل عند بدء الخيط، فلربما سيرى الخيط تلك القيمة بمجرد بدءه، ويُفسِّرها على كونها إشارةً للتوقُّف قبل أن يفعل أي شيء. تذكَّر أنه عند استدعاء runner.start()‎، يبدأ الخيط runner بالعمل على التوازي مع الخيط المُستدعِي له.

عندما ينقر المُستخدِم على زر "Finish"، تُضبَط قيمة running إلى القيمة false إشارةً للخيط بأن عليه الانتهاء، ولكن ماذا لو كان الخيط نائمًا في تلك اللحظة؟ في تلك الحالة لا بُدّ أن يستيقظ الخيط أولًا حتى يتمكَّن من الإستجابة لتلك الإشارة؛ أما إذا أردنا أن نجعله يستجيب بصورةٍ أسرع، يُمكِننا استدعاء التابع runner.interrupt()‎ لإيقاظ الخيط إذا كان نائمًا. لا يؤثر هذا على البرنامج من الناحية العملية، ولكنه يجعل استجابة البرنامج أسرع على نحوٍ ملحوظ بالأخص إذا نقر المُستخدِم على زر "Finish" بعد النقر على زر "Start" مباشرةً عندما ينام الخيط لمدة ثانيةٍ كاملة.

استخدام الخيوط بالعمليات المنفذة بالخلفية

إذا أردنا أن تكون استجابة برامج واجهة المُستخدِم الرسومية GUI سريعة، أي تستجيب للأحداث events بمجرد وقوعها تقريبًا، لا بُدّ أن تُنهِي توابع معالجة الأحداث الموجودة بالبرنامج عملها بسرعة. تُخزَّن الأحداث برتل queue أثناء وقوعها، ولا يستطيع الحاسوب الاستجابة لحدثٍ معين قبل أن تُنهِي توابع معالجة الأحداث السابقة له عملها. يَعنِي ذلك أنه ينبغي للأحداث الانتظار أثناء تنفيذ الحاسوب لمُعالِج حدثٍ معين؛ وإذا استغرق مُعالِج حدثٍ معين فترةً طويلة لتنفيذ عمله، ستَجْمُد freeze واجهة المُستخدِم خلال تلك الفترة، وهو ما يُضايق المُستخدِم بالأخص إذا استمر لأكثر من جزءٍ من الثانية.

تستطيع الحواسيب العصرية لحسن الحظ إنجاز الكثير من العمليات الضخمة خلال جزءٍ من الثانية. ومع ذلك، هناك بعض العمليات الضخمة للغاية لدرجة لا يُمكِن تنفيذها بمعالجات الأحداث event handlers (أو بتمرير مُنفَّذات للواجهة Runnable إلى Platform.runlater()‎). ويكون من الأفضل في تلك الحالة تنفيذ تلك العمليات بخيطٍ آخر منفصل يَعمَل على التوازي مع خيط معالجة الأحداث، وهذا يَسمَح للحاسوب بالاستجابة إلى الأحداث الأخرى في نفس الوقت الذي يُنفَّذ خلاله تلك العملية، ويُقال أن العملية "تُنفَّذ بالخلفية background".

يختلف تطبيق الخيوط في هذا المثال عن المثال السابق؛ فعندما يُستخدَم الخيط للتحكُّم بتحريكة، فإنه فعليًا لا يَفعَل سوى القليل، إذ عليه فقط أن يستيقظ كل عدة ثواني ليُجرِي قليلًا من العمليات المتعلّقة بتحديث متغيرات الحالة state variables لإطار التحريكة التالي ومن ثَمّ رَسْمه. يُوفِّر ذلك وقتًا كافيًا لخيط تطبيق جافا إف إكس، ويَسمَح له بإجراء أي إعادة رسمٍ ضرورية لمُكوِّنات واجهة المُستخدِم الرسومية، وكذلك معالجة أي أحداثٍ اخرى.

عندما نُنفِّذ عمليةٌ معينة بالخلفية ضمن خيط، فإننا نريد إبقاء الحاسوب مُنشغِلًا بتنفيذ تلك العملية بأقصى ما يمكن، ولكن قد يتسابق هذا الخيط مع خيط التطبيق على زمن المعالجة، وعندها يبقى تعطُّل معالجة الأحداث بالأخص إعادة الرسم ممكنًا إذا لم ننتبه كفاية. يُمكِننا لحسن الحظ استخدام أولويات priorities للخيوط لنتجنَّب تلك المشكلة، حيث يُمكِننا ضبط الخيط المسؤول عن تنفيذ العملية بالخلفية بحيث يَعمَل بأولويةٍ أقل من أولوية خيط معالجة الأحداث، وسنضمَن بذلك معالجة الأحداث بأسرع ما يمكن، وسيَحظَى بنفس الوقت الخيط الآخر بأي زمن معالجةٍ إضافي. تستغرق معالجة الأحداث وقتًا قصيرًا للغاية عمومًا، وبالتالي سيُستغَل غالبية زمن المعالجة بتنفيذ العملية المُشغَّلة بالخلفية بنفس الوقت الذي ستستجيب فيه الواجهة بسرعة. ناقشنا أولوية الخيوط في قسم العمليات على الخيوط من المقال السابق.

يُعدّ البرنامج BackgroundComputationDemo.java مثالًا توضيحيًا على معالجة العمليات بالخلفية. يُنشِئ هذا البرنامج صورةً يستغرِق تحديد ألوان بكسلاتها وقتًا طويلًا نوعًا ما. تمثّل تلك الصورة قطعةً من شكلٍ هندسيٍ معروف باسم "مجموعة ماندلبرو Mandelbrot set"، وسنستخدِم تلك الصورة بعدة أمثلة خلال هذا المقال.

يتشابه البرنامج "BackgroundComputationDemo" مع برنامج "QuicksortThreadDemo" الذي ناقشناه بالأعلى، حيث تُجرَى العمليات ضمن خيطٍ مُعرَّفٍ بواسطة صنفٍ مُتداخِل اسمه Runner، وسنَستخدِم متغيرًا متاطيرًا اسمه running للتحكُّم بالخيط؛ فإذا كان المُتغيّر يساوي false، ينبغي أن ينتهي الخيط. يُوفِّر البرنامج زرًا لبدء العملية وإنهائها. بخلاف البرنامج السابق، سيَعمَل الخيط باستمرار دون أن ينام. بعد انتهاء الخيط من حساب كل صف من البسكلات، سيَستدعِي التابع Platform.runLater()‎ لينسخ تلك البكسلات إلى الصورة المعروضة على الشاشة، وسيتمكَّن بذلك المُستخدِم من مشاهدة التحديثات الناتجة عن العمليات المُجرَاة، وسيشاهد الصورة أثناء تكوُّنها صفًا بعد آخر.

عندما ينقر المُستخدِم على زر "Start"، يُنشَأ الخيط المسؤول عن عملية المعالجة، والذي لا بُدّ من ضبطه ليَعمَل بأولويةٍ أقل من أولوية خيط تطبيق جافا إف إكس. نظرًا لوقوع الشيفرة المسؤولة عن إنشاء ذلك الخيط ضمن خيط التطبيق، يُمكِننا ضبط أولوية الخيط المُنشَا بحيث تكون أقل بمقدار الواحد من أولوية الخيط المُشغَّل. تذكَّر أنه من الضروري ضبط تلك الأولوية داخل تعليمة try..catch؛ فإذا حدث خطأ أثناء ذلك، نَضمَن استمرار البرنامج، وإن لم يَكُن بنفس السلاسة التي كان سيَعمَل بها لو كانت الأولوية قد ضُبطَت صحيحًا. تُنشِئ الشيفرة التالية الخيط، وتُشغِّله:

runner = new Runner();
try {
    runner.setPriority( Thread.currentThread().getPriority() - 1 );
}
catch (Exception e) {
    System.out.println("Error: Can't set thread priority: " + e);
}
running = true;  // اضبط قيمة الإشارة قبل تشغيل الخيط
runner.start();

على الرغم من عمل البرنامج BackgroundComputationDemo جيدًا، إلا أن هناك مشكلةً واحدةً وهي أن هدفنا هو إتمام العملية بأسرع وقتٍ ممكن من خلال استغلال ما هو مُتوفّرٌ من زمن المعالجة. سيتمكَّن البرنامج من إنجاز ذلك الهدف إذا كان مُشغَّلًا على حاسوبٍ بمعالج واحد؛ ولكن إذا كان الحاسوب يحتوي على عدة معالجات، فإننا في الواقع نَستخدِم معالجًا واحدًا فقط لإنجاز العملية، وفي تلك الحالة، لن يكون لأولوية الخيط أي أهمية؛ فمن الممكن تشغيل كُلٍ من خيط التطبيق وخيط التحريكة على التوازي باستخدام معالجين مختلفين. سيكون من الأفضل لو تمكَّنا من استخدام جميع تلك المعالجات لإتمام العملية، وهو ما يتطلَّب معالجةً على التوازي parallel processing من خلال عدة خيوط. سننتقل إلى تلك المشكلة فيما يلي.

استخدام الخيوط في المعالجة المتعددة

سنفحص الآن البرنامج MultiprocessingDemo1.java، الذي يختلف قليلًا عن البرنامج "BackgroundComputationDemo"؛ فبدلًا من إجراء المعالجة بخيطٍ واحد فقط، سيُقسِّم البرنامج "MultiprocessingDemo1" المعالجة على عدة خيوط. وسيَسمَح البرنامج للمُستخدِم بتخصيص عدد الخيوط المطلوب تشغيلها، وسيتولى كل خيطٍ منها المعالجة المطلوبة لجزءٍ معين من الصورة. ينبغي أن تنجز الخيوط عملها على التوازي؛ فإذا استخدمنا خيطين على سبيل المثال، فسيَحسِب الأول النصف الأعلى من الصورة؛ بينما سيَحسِب الثاني النصف السفلي. تعرض الصورة التوضيحية التالية شاشة البرنامج مع اقتراب نهاية المعالجة عند استخدام ثلاثة خيوط، حيث تُشير المساحات الرمادية إلى أجزاء الصورة غير المُعالجة بعد.

001Multiprocessing_Demo1.png

عليك أن تُجرِّب البرنامج؛ فعند استخدام عدة خيوطٍ بحاسوبٍ متعدّد المعالجات، ستكتمل المعالجة على نحوٍ أسرع بالموازنة مع استخدام خيطٍ واحد.

اقتباس

 

ملاحظة: سيَعمَل هذا البرنامج بنفس طريقة عمل برنامج المثال السابق في حالة استخدام خيط واحد.

 

لا تُعدّ الطريقة المُستخدَمة لتقسيم المشكلة على مجموعة الخيوط بهذا المثال الطريقة الأمثل، وسنتناول بالقسم التالي إمكانية تحسين تلك الطريقة، ومع ذلك ما يزال البرنامج "MultiprocessingDemo1" مثالًا جيدًا على المعالجة المُتعدّدة.

عندما ينقر المُستخدِم على زر "Start"، سيُنشِئ البرنامج عدد الخيوط المُخصَّصة ويُشغِّلها، كما سيُسنِد لكُلٍ منها جزءًا من الصورة. ألقِ نظرةً على الشيفرة التالية:

workers = new Runner[threadCount];  // يحمل خيوط المعالجة
int rowsPerThread;  // عدد الخيوط التي ينبغي أن يحسبها كل خيط
rowsPerThread  = height / threadCount;  // (height = vertical size of image)
running = true;  // اضبط قيمة الإشارة قبل تشغيل الخيوط
threadsCompleted = 0;  // Records how many of the threads have terminated.
for (int i = 0; i < threadCount; i++) {
    int startRow;  // ‫الصف الأول الذي يحسبه الخيط رقم i‫
    int endRow;    // 1
    startRow = rowsPerThread*i;
    if (i == threadCount-1)
        endRow = height-1;
    else
        endRow = rowsPerThread*(i+1) - 1;
    workers[i] = new Runner(startRow, endRow);
    try {
        workers[i].setPriority( Thread.currentThread().getPriority() - 1 );
    }
    catch (Exception e) {
    }
    workers[i].start();
}

[1] الصف الأخير الذي يحسبه الخيط رقم i. انشِئ خيطًا وشغَِله لحساب صفوف الصورة من startRow إلى endRow. لا بُدّ أن تكون قيمة endRow للخيط الأخير مساويةً لرقم الصف الأخير بالصورة.

أجرينا عددًا قليلًا من التعديلات لتفعيل المعالجة المتعددة إلى جانب إنشاء عدة خيوط بدلًا من خيط واحد. كما هو الحال في المثال السابق: عندما ينتهي أي خيطٍ من حساب ألوان صفٍ من البسكلات، فإنه يَستدعِي التابع Platform.runLater()‎ ليَنسَخ الصف إلى الصورة.

هناك شيءٌ واحدٌ جديد، وهو: عندما تنتهي جميع الخيوط من عملها، سيتبدّل اسم الزر من "Abort" إلى "Start Again"، وسيُعاد تفعيل القائمة التي كانت قد عُطّلَت بينما الخيوط مُشغَّلة. والآن، كيف سنعرف أن جميع الخيوط قد انتهت؟ ربما تتساءل لما لا نَستخدِم join()‎ لننتظر انتهاء الخيوط كما فعلنا بمثالٍ سابق في قسم العمليات على الخيوط من المقال السابق؟ حيث لا يُمكِننا بالتأكيد فعل ذلك بخيط تطبيق جافا إف إكس على الأقل.

سنَستخدِم في هذا المثال متغير نسخة instance variable، اسمه threadsRunning لتمثيل عدد خيوط المعالجة قيد التنفيذ؛ وعندما ينتهي كل خيطٍ من عمله، عليه استدعاء تابعٍ لإنقاص قيمة ذلك المتغير بمقدار الواحد، حيث سيُستدعَى ذلك التابع ضمن عبارة finally بتعليمة try لنتأكَّد تمامًا من تنفيذها عند انتهاء الخيط. عندما يُصبِح عدد الخيوط المُشغَّلة صفرًا، سيُحدِّث التابع حالة البرنامج على النحو المطلوب. تعرض الشيفرة التالية التابع الذي ستستدعيه الخيوط قبل انتهائها:

synchronized private void threadFinished() {
    threadsRunning--;
    if (threadsRunning == 0) { // انتهت جميع الخيوط
        Platform.runLater( () -> {
               // تأكَّد من صحة حالة واجهة المُستخدِم الرسومية عندما تنتهي الخيوط
            startButton.setText("Start Again");
            startButton.setDisable(false);
            threadCountSelect.setDisable(false);
        });
        running = false;
        workers = null;
    }
}

لاحِظ أن التابع المُعرَّف بالأعلى متزامن synchronized؛ لضمان تجنُّب حالة التسابق race condition التي يُمكِنها أن تقع عند إنقاص قيمة المُتغيّر threadsRunning. قد يَستدعِي خيطان ذلك التابع بنفس اللحظة إذا لم نَستخدِم المزامنة، وإذا كان التوقيت دقيقًا، فربما يقرأ كلاهما نفس قيمة المتغير threadsRunning، ويَحسِبا نفس الإجابة بعد إنقاصه. ستقل في تلك الحالة قيمة المتغير threadsRunning بمقدار واحدٍ فقط وليس اثنين؛ أي أننا لم نَعُدّ خيطًا على النحو الصحيح، وعليه لن يَصِل المتغير threadsRunning إلى الصفر نهائيًا، وسيَعمَل البرنامج باستمرار بطريقةٍ مشابهة للقفل الميت deadlock. في الواقع، نادرًا ما تَحدث تلك المشكلة لأنها تعتمد على توقيتٍ بعينه، ولكن بالبرامج الأكبر حجمًا، تصبح تلك المشاكل خطيرةً للغاية كما أن تنقيحها debug ليس سهلًا. في المقابل، تمنع المزامنة حدوث ذلك الخطأ تمامًا.

ترجمة -بتصرّف- للقسم Section 2: Programming with Threads من فصل Chapter 12: Threads and Multiprocessing من كتاب Introduction to Programming Using Java.

اقرأ أيضًا

 


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...