چرا سکان آکادمی؟
آشنایی با نحوۀ کنترل ترتیب اجرای تِرِدها در زبان برنامه‌نویسی جاوا

آشنایی با نحوۀ کنترل ترتیب اجرای تِرِدها در زبان برنامه‌نویسی جاوا

در آموزش‌ گذشته با مفاهیم تِرِد و مالتی‌تِرِدینگ در زبان برنامه‌نویسی جاوا آشنا شده و نحوۀ ایجاد دو تِرِد و اجرای آن‌ها به صورت هم‌زمان را آموختیم اما در این آموزش قصد داریم تا روشی به منظور کنترل ترتیب اجرای تِرِدهای برنامه ارائه نماییم. همان‌طور که در آموزش آشنایی با مفهوم Thread در زبان برنامه‌نویسی جاوا توضیح دادیم، دو روش برای ساخت تِرِد در زبان جاوا وجود دارد که در این آموزش از روش اول به منظور ایجاد تِرِدها استفاده کرده و بدین ترتیب یکسری کلاس جهت ارث‌بری از کلاس Thread زبان جاوا تعریف کرده و در ادامه نحوۀ کنترل ترتیب اجرای تِرِدها را مورد بررسی قرار خواهیم داد.

در همین راستا، ابتدا پروژه‌ای تحت عنوان ControllingThreadExecution در محیط برنامه‌نویسی اکلیپس ایجاد کرده و در آن کلاسی به نام HowToControlThreads می‌سازیم به طوری که در کد زیر داریم:

public class HowToControlThreads extends Thread {
    @Override
    public void run() {
        System.out.println("This is text from first thread 1");
        System.out.println("This is text from first thread 2");
        System.out.println("This is text from first thread 3");
        System.out.println("This is text from first thread 4");
        System.out.println("This is text from first thread 5");
        System.out.println("This is text from first thread 6");
    }
}

در کد فوق، ابتدا نام کلاس را نوشته و در ادامه کیورد extends سپس نام کلاس Thread را نوشته‌ایم که بدین ترتیب کلاس HowToControlThreads از کلاس Thread ارث‌بری کرده و در ادامه متد ()run از این کلاس را جهت پیاده‌سازی دستورات مد نظر خود اُورراید کرده‌ایم و در آن دستوراتی مبنی بر چاپ استرینگ‌های فوق را نوشته‌ایم که در صورت فراخوانی متد ()run استرینگ‌های مربوطه را در کنسول نمایش می‌دهد.

اکنون برای آنکه بتوانیم تِرِدی ایجاد کرده و برنامۀ فوق را با آن اجرا کنیم، می‌باید کلاس دیگری تحت عنوان ActionClass ساخته و در حین ساخت کلاس نیز تیک گزینۀ public static void main را بزنیم چرا که این کلاس به منزلۀ نقطۀ شروع برنامه می‌باشد. در ادامه، آبجکتی از روی کلاس HowToControlThreads ساخته و به روش زیر متد ()start را روی آبجکت جدید فراخوانی می‌نماییم:

public class ActionClass {
    public static void main(String[] args) {
        HowToControlThreads myObject1 = new HowToControlThreads();
        myObject1.start();
    }
}

در کد فوق، داخل متد ()main آبجکت جدیدی به نام myObject1 از روی کلاس HowToControlThread ایجاد کرده سپس متد ()start را روی آن فراخوانی کرده‌ایم که بدین ترتیب یک تِرِد جدید ایجاد شده و متد ()run از کلاس HowToControlThread نیز فراخوانی شده و اجرای تَسک مربوط به متد ()run از این کلاس به تِرِد ایجادشده واگذار می‌شود. بنابراین با اجرای برنامۀ فوق در خروجی خواهیم داشت:

This is text from first thread 1
This is text from first thread 2
This is text from first thread 3
This is text from first thread 4
This is text from first thread 5
This is text from first thread 6

همان‌طور که می‌بینیم، تنها تِرِد ایجادشده در برنامه دستورات مد نظر را به ترتیب اجرا کرده و منجر به چاپ استرینگ‌های فوق می‌گردد. اکنون تِرِدی دیگر ایجاد می‌کنیم تا بتوانیم نحوۀ کنترل ترتیب اجرای آن‌ها را بررسی نماییم و برای این منظور کلاس دیگری تحت عنوان MySecondThread ایجاد کرده و به روش زیر متد ()run را در آن اُورراید می‌نماییم:

public class MySecondThread extends Thread {
    @Override
    public void run() {
        System.out.println("This is text from second thread 1");
        System.out.println("This is text from second thread 2");
        System.out.println("This is text from second thread 3");
        System.out.println("This is text from second thread 4");
        System.out.println("This is text from second thread 5");
        System.out.println("This is text from second thread 6");
    }
}

در کد فوق، کلاسی تحت عنوان MySecondThread ایجاد کرده و در ادامه کیورد extends را به منظور ارث‌بری کلاس مذکور از کلاس Thread درج کرده‌ایم سپس داخل متد ()run شش دستور مبنی بر چاپ استرینگ‌های مربوطه نوشته‌ایم که بدین ترتیب در صورت ساخت تِرِد جدید و فراخوانی متد ()run از این کلاس، استرینگ‌های مذکور در کنسول نمایش داده می‌شوند. حال می‌باید آبجکتی از روی کلاس MySecondThread داخل کلاس ActionClass ایجاد کنیم که برای این منظور کد مربوط به کلاس ActionClass را به صورت زیر تکمیل می‌نماییم:

public class ActionClass {
    public static void main(String[] args) {
        HowToControlThreads myObject1 = new HowToControlThreads();
        myObject1.start();
        MySecondThread myObject2 = new MySecondThread();
        myObject2.start();
    }
}

در کد فوق، آبجکتی تحت عنوان myObject2 از روی کلاس MySecondThread ایجاد کرده سپس متد ()start از کلاس Thread را به منظور ایجاد یک تِرِد جدید و همچنین فراخوانی متد ()run از کلاس MySecondThread فراخوانی نموده‌ایم. بنابراین در نتیجۀ فراخوانی متد ()start تِرِدی جدید ایجاد شده و اجرای دستورات داخلی متد ()run بدان واگذار می‌شود که با اجرای مجدد برنامۀ فوق در خروجی خواهیم داشت:

This is text from first thread 1
This is text from second thread 1
This is text from second thread 2
This is text from second thread 3
This is text from first thread 2
This is text from second thread 4
This is text from second thread 5
This is text from second thread 6
This is text from first thread 3
This is text from first thread 4
This is text from first thread 5
This is text from first thread 6

همان‌طور که می‌بینیم، هر دو تِرِد دستورات مربوط به متد ()run از کلاس متناظرشان را به صورت هم‌زمان اجرا کرده‌اند به طوری که در ابتدا تِرِد اول دستور پرینت اول از متد ()run در کلاس HowToControlThreads را اجرا کرده سپس تِرِد دوم با در اختیار داشتن منابع سیستم دستور پرینت اول، دوم و سوم از متد ()run از کلاس MySecondThread را اجرا نموده است. در ادامه، مجدداً تِرِد اول دستور پرینت دوم از متد ()run در کلاس HowToControlThreads را اجرا کرده است و به همین منوال می‌بینیم که دستورات مرتبط با هر تِرِد بدون رعایت ترتیب خاصی اجرا شده‌اند. به علاوه، ترتیب اجرای دستورات برنامه توسط تِرِدها نیز کاملاً تصادفی بوده و ممکن است در هر بار اجرای برنامه نتایج مختلفی را به دست آوریم.

در همین راستا، به منظور کنترل ترتیب اجرای دستورات توسط تِرِدهای ایجادشده در برنامه در ادامه قصد داریم تا اینترفیسی از پیش تعریف‌شده تحت عنوان ExecutorService در زبان برنامه‌نویسی جاوا را معرفی نماییم.

نحوۀ کنترل ترتیب اجرای تِرِدها با استفاده از اینترفیس ExecutorService 

ExecutorService یک اینترفیس به اصطلاح Built-in در زبان جاوا می‌باشد که قابلیت محدود کردن برنامه به اجرای تَسک‌های آن به صورت اصطلاحاً Asynchronous یا «غیرهم‌زمان» را به دولوپرها می‌دهد تا بدین طریق بتوانند کنترل بیشتری بر ترتیب اجرای تَسک‌های مد نظر با به‌کارگیری تِرِدها را داشته باشند. در واقع، اینترفیس ExecutorService دارای کلاس‌ها و متدهایی است که با استفاده از آن‌ها می‌توان تعداد تِرِدهای مورد نیاز برای اجرای برنامه، نحوۀ واگذاری تَسک‌ها به هر یک از تِرِدها و همچنین ترتیب اجرای آن‌ها را کنترل نمود.

به علاوه، از آنجایی که ExecutorService یک اینترفیس است، به منظور دستیابی به کلاس‌ها و متدهایی که این اینترفیس را ایمپلیمنت کرده‌اند می‌باید به پکیج از پیش تعریف‌شدۀ concurrent و کلاس Executors از این پکیج دست پیدا کنیم که برای این منظور نیز هر دو مورد (اینترفیس ExecutorService و کلاس Executors) را مشابه آنچه در آموزش معرفی کلاس Scanner در زبان برنامه‌نویسی جاوا آموختیم، در برنامۀ خود ایمپورت می‌نماییم.

در واقع، با ساخت نمونه‌ای از اینترفیس ExecutorService می‌توانیم به متد مورد نیاز خود از کلاس Executors دست یافته و آن را فراخوانی کنیم که برای این منظور کد فوق را به صورت زیر تکمیل می‌نماییم:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ActionClass {
    public static void main(String[] args) {
        HowToControlThreads myObject1 = new HowToControlThreads();
        // myObject1.start();
        MySecondThread myObject2 = new MySecondThread();
        // myObject2.start();
        ExecutorService myController = Executors.newSingleThreadExecutor();
        myController.submit(myObject1);
        myController.submit(myObject2);
    }
}

همان‌طور که در کد فوق می‌بینیم، اینترفیس ExecutorService و کلاس Executors را ایمپورت کرده‌ایم. در ادامه می‌باید دستور مربوط به اجرای تِرِدها را متوقف کنیم که برای این منظور سطر ششم و هشتم از کد فوق را کامنت می‌کنیم. در سطر بعد، یک آبجکت از روی اینترفیس ExecutorService به نام myController تعریف کرده و آن را معادل فراخوانی متد ()newSingleThreadExecutor از کلاس Executors قرار داده‌ایم و نحوۀ کار متد ()newSingleThreadExecutor بدین صورت است که یک به اصطلاح Single Thread ایجاد کرده و اجرای تَسک‌های برنامه را محدود به همان یک تِرِد می‌نماید. در واقع، در سطر هشتم یک به اصطلاح Executor یا «اجراکننده» به نام myController ساخته‌ایم که با استفاده از تنها یک تِرِد تَسک‌های محول‌شده به آن را اجرا می‌نماید.

در ادامه، متد ()submit از این کلاس را روی myController فراخوانی می‌نماییم که نحوۀ کار متد ()submit بدین ترتیب است که یک آرگومان ورودی از جنس آبجکت ساخته‌شده از روی یک کلاس گرفته و متد ()run از کلاس مربوطه را فراخوانی می‌نماید. در همین راستا، در سطر دهم و یازدهم نیز به ترتیب آبجکت‌های  myObject1 و myObject2 را به عنوان ورودی به متد ()submit داده و آن‌ها را روی myController فراخوانی کرده‌ایم که بدین ترتیب متدهای ()run از هر دو کلاس فراخوانی شده و اجرای آن‌ها به ترتیب روی تنها یک تِرِد انجام می‌شود بدین معنی که تِرِد ایجادشده ابتدا متد ()run از کلاس HowToControlThreads را به طور کامل اجرا کرده سپس به اجرای متد ()run از کلاس MySecondThread می‌پردازد که برای درک بهتر این مسئله، برنامۀ فوق را اجرا کرده و خروجی را مشاهده خواهیم کرد:

This is text from first thread 1
This is text from first thread 2
This is text from first thread 3
This is text from first thread 4
This is text from first thread 5
This is text from first thread 6
This is text from second thread 1
This is text from second thread 2
This is text from second thread 3
This is text from second thread 4
This is text from second thread 5
This is text from second thread 6

همان‌طور که می‌بینیم، در ابتدا دستورات مربوط به متد ()run از کلاس HowToControlThreads به طور کامل انجام شده و در ادامه دستورات متد ()run از کلاس MySecondThread اجرا شده‌اند به طوری که ترتیب اجرای برنامه‌ها نیز درست به همان ترتیبی می‌باشد که آبجکت‌های ساخته‌شده از روی هر دو کلاس را به متد ()submit پاس داده بودیم. حال اگر بخواهیم این مسئله را با حالت پیش و بدون استفاده از قابلیت‌های کلاس Executors مقایسه کنیم، می‌توانیم بخش مربوطه از کد فوق را به صورت زیر ویرایش کنیم:

public class ActionClass {
    public static void main(String[] args) {
        HowToControlThreads myObject1 = new HowToControlThreads();
        myObject1.start();
        MySecondThread myObject2 = new MySecondThread();
        myObject2.start();
        // ExecutorService myController = Executors.newSingleThreadExecutor();
        // myController.submit(myObject1);
        // myController.submit(myObject2);
    }
}

همان‌طور که می‌بینیم، ابتدا سطرهای مربوط به فراخوانی متدهای ()start روی آبجکت‌های ساخته‌شده از روی هر دو کلاس را از حال کامنت خارج کرده سپس بخش مربوط به اینترفیس ExecutorService را کامنت کرده‌ایم و مجدداً برنامه را اجرا می‌کنیم به طوری که در خروجی خواهیم داشت:

This is text from first thread 1
This is text from first thread 2
This is text from first thread 3
This is text from second thread 1
This is text from second thread 2
This is text from second thread 3
This is text from second thread 4
This is text from second thread 5
This is text from second thread 6
This is text from first thread 4
This is text from first thread 5
This is text from first thread 6

همان‌طور که ملاحظه می‌کنیم، هر دو تِرِد دستورات متدهای ()run را بدون ترتیب خاصی اجرا کرده‌اند که این نتیجه کاملاً تصادفی بوده و به احتمال بسیار زیاد در اجرای مجدد برنامۀ فوق نتایج متفاوتی را به دست آوریم. 

در این آموزش ابتدا به بررسی ترتیب اجرای تِرِدها در برنامه‌های پرداختیم که به صورت مالتی‌تِرِد اجرا می‌شوند سپس به منظور کنترل ترتیب اجرای تَسک‌ها توسط تِرِدهای ایجادشده، اینترفیسی تحت عنوان ExecutorService را معرفی کرده و گفتیم که جهت دسترسی به متدهای پیاده‌سازی‌شدۀ این اینترفیس، به کلاس Executors مراجعه کرده و متدهای مورد نیاز خود را فراخوانی می‌نماییم. در ادامه، متد ()newSingleThreadExecutor را فراخوانی کردیم که این وظیفه را دارا است تا یک تِرِد ایجاد کرده و اجرای تمامی تَسک‌های برنامه را محدود به آن تِرِد نماید.