تعدد المسالك Multi-Threading في لغة الجافا – القسم الثالث

الدالة Thread.join

لشرح الدالة join سأتابع من حيث توقفنا في المرة الماضية ليكن لدينا الشيفرة التالية:

/**
 *
 * @author Mutasem
 */
public class MainClass {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("counter befor thread 1 start: " + MyThread.counter);
        MyThread t1 = new MyThread();
        t1.start();
        //t1.join();
        System.out.println("counter befor thread 2 start: " + MyThread.counter);
        MyThread t2 = new MyThread();
        t2.start();
        //t2.join();
        System.out.println("counter at the end of main " + MyThread.counter);

    }
}

إن من قام بتنفيذ البرنامج سيلاحظ اختلاف النتيجة في كل مرة يتم فيها التنفيذ، فبعض الأحيان يكون الناتج 0، وفي كثير من الأحيان 10، وقد يحدث ان تجد الناتج 20 فما الذي أدى إلى هذا الاختلاف.

قم بإلغاء تعليق التعليمتين join ستلاحظ أن النتيجة ستكون دوماً 20 فما الذي حدث؟

كما قلت في مقدمة السلسلة لكل برنامج خيط واحد على الأقل يسمى الخيط الرئيسي ومنه يمكن إنشاء عدد من الخيوط الفرعية.

وقلنا في حالة تعدد الخيوط يقوم كل خيط بتنفيذ الكود خاص به دون الحاجة لانتظار بقية الخيوط.

  • في بداية تشغيل البرنامج count=0
  • قمنا بتشغيل الخيط t1 والذي بدأ بزيادة العداد count 1 ثم 2 ثم…
  • في لحظة ما قبل أن ينهي الخيط t1 عمله قمنا بتشغيل الخيط t2 والذي أيضاً بدأ بدوره بزيادة العداد
  • ولكن في لحظة ما وقبل أن ينهي الخيط t2 عمله قمنا بطباعة الناتج على الشاشة وإنهاء البرنامج

والذي حدث فعلياً أن الخيط الرئيسي أنهى عمله قبل نهاية الخيوط الفرعية وكون آخر عملية طباعة كانت موجودة ضمن الخيط الرئيسي فلم يعد ممكنا مشاهدة النتيجة بعد انتهاء الخيط الرئيسي من العمل.

أما عند استدعاء الدالة t1.join فإن الخيط الرئيسي يتوقف عن العمل بانتظار الخيط t1 وبعد انتهاء t1 يتابع الخيط الرئيسي عمله، ومع استدعاء t2.join يضطر الخيط الرئيسي للانتظار مرة أخرى حتى ينهي t2 عمله وبالتالي ستكون تعليمة الطباعة النهائية دوماً بعد نهاية الخيطين t1 وt2 وسيكون الناتج حتماً 20.

هل تتوقف الخيوط الفرعية عن العمل بعد انتهاء الخيط الرئيسي؟

في الحقيقة لا تتوقف الخيوط الفرعية عن العمل حتى بعد انتهاء الخيط الرئيسي وللتأكد من ذلك أعد تعليق التعليمتين t1.join() و t2.join() ثم قم بتعديل الصف MyThread ليصبح بالشكل التالي

package com.mutasemhajhasan.threading.example1;

/**
 *
 * @author Mutasem
 */
public class MyThread extends Thread {

    public static int counter;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            counter++;
        }
        System.out.println("Counter " + counter);
    }

}
/*
 * counter befor thread 1 start: 0
 * counter befor thread 2 start: 0
 * counter at the end of main 0
 * Counter 10
 * Counter 20
 */

لاحظ كيف تمت عملية الطباعة حتى بعد انتهاء تنفيذ البرنامج.

لكن بقي السؤال لماذا لا يظهر في الطباعة سوى الأرقام 0, 10, 20 لماذا لم نشاهد مثلاً الرقم 3 أو الرقم 15؟

الدالة Thread.sleep

تقوم هذه الدالة بجعل الخيط الحالي يتوقف عن العمل لمدة محددة بالوسيط الممر إليها مقدراً بالميلي ثانية ويتابع الخيط عمله بعد انتهاء المدة.

باستخدام هذه الدالة سأقوم بتعديل المشروع السابق بحيث نتمكن من مشاهدة التغيرات بشكل أوضح

الصف MyThread

package com.mutasemhajhasan.threading.example1;

import java.util.logging.Level;
import java.util.logging.Logger;



/**
 *
 * @author Mutasem
 */
public class MyThread extends Thread {

    public  static int counter;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            counter++;
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
                Logger.getLogger(MyThread.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        System.out.println("Counter " + counter);
    }

}

فقط قمنا بإضافة تأخير زمني مقدار 100 ميلي ثانية بعد كل عملية زيادة ليصبح بذلك الزمن الكلي لتنفيذ المسار تقريباً يساوي ثانية كاملة.

تعديل الصف MainClass

package com.mutasemhajhasan.threading.example1;

/**
 *
 * @author Mutasem
 */
public class MainClass {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("counter befor thread 1 start: " + MyThread.counter);
        MyThread t1 = new MyThread();
        t1.start();
        Thread.sleep(200);
        ;
        // t1.join();
        System.out.println("counter befor thread 2 start: " + MyThread.counter);
        MyThread t2 = new MyThread();
        t2.start();
        Thread.sleep(200);
        System.out.println("counter after thread 2 start " + MyThread.counter);
        // t2.join();
        Thread.sleep(2000);
        System.out.println("counter at the end of main " + MyThread.counter);

    }
}

بعد تشغيل الخيط الأول t1.start قمت بجعل الخيط الرئيسي يتوقف عن العمل لمدة 200 ميلي ثانية، في هذه الأثناء يكون الخيط الأول قد زاد العداد بمقدار 2.

بعد تشغيل الخيط الثاني أيضاً جعلت الخيط الرئيسي ينتظر 200 ميلي ثانية قبل القيام بعملية الطباعة لكن الخيطين الفرعين لازالا يعملان في الخلفية وكل منهما سيقوم بزيادة بمقدار 2 بالتالي يصبح الناتج 2+2+2=6.

أخيراً قبل عملية الطباعة الأخيرة جعلنا الخيط الرئيسي ينتظر مدة 2000 ميلي ثانية للتاكد أن الخيطين قد أنهيا عملهما بالكامل وسنلاحظ عندها أن آخر تعليمة طباعة تحمل القيمة 20.

counter befor thread 1 start: 0
counter befor thread 2 start: 3
counter after thread 2 start 8
Counter 19
Counter 20
counter at the end of main 20

يمثل الخرج السابق تنفيذ الشيفرة على جهازي الخاص، نلاحظ اختلافاً عن الأرقام التي قمت بحسابها فبدل الرقم 2 ظهر 3، وبدل 6 ظهر 8، والسبب في ذلك أن الدالة sleep تعتمد على آلية غير دقيقة لحساب التأخير الزمني فلو طلبت تأخيراً زمنياً بمقدار 3 ثوان مثلاً قد أحصل على تأخير أقل أو أكثر قليلاً، أيضاً لا ننسى أن الخيوط تتقاسم حصة البرنامج من المعالج وهذه الحصة ليست ثابتة بل تختلف حسب سرعة المعالج، وحسب انشغاله في برامج أخرى.

قد تلاحظ أن الأمور اختلطت عليك قليلاً بعد قراءة هذه المقالة، لا تقلق فبعد تنفيذ البرنامج على جهازك عدة مرات ومتابعة الشيفرة البرمجية ستتقن هذه الأمور بشكل جيد.

تعمدت إثارة هذه المشاكل في مقالتي كي أضع القارئ أمام أكبر عدد ممكن من الاحتمالات التي قد تصادفه أثناء برمجة الخيوط، ولأحصل على فهم عميق لفكرة تعدد الخيوط.

رابط السلسة على GitHub

https://github.com/mutasemhajhasan/Java-Multi-Threading

المراجع

Herbert Schildt. (2014). Java: the Complete Reference 9th Edition. New York, Chicago, San Francisco, Athens, London, Madrid, Mexico City: McGraw-Hill Education.