در آموزش گذشته با مفاهیم تِرِد و مالتیتِرِدینگ در زبان برنامهنویسی جاوا آشنا شده و نحوۀ ایجاد دو تِرِد و اجرای آنها به صورت همزمان را آموختیم اما در این آموزش قصد داریم تا روشی به منظور کنترل ترتیب اجرای تِرِدهای برنامه ارائه نماییم. همانطور که در آموزش آشنایی با مفهوم 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
را فراخوانی کردیم که این وظیفه را دارا است تا یک تِرِد ایجاد کرده و اجرای تمامی تَسکهای برنامه را محدود به آن تِرِد نماید.