در این آموزش یکی از مباحث بسیار مهم در زبان برنامهنویسی جاوا تحت عنوان Thread را معرفی کرده و مقدمهای از کاربرد آن در اجرای تَسکهای نوشتهشده در برنامههای جاوا را تشریح میکنیم. به طور کلی، میتوان گفت که تِرِدها دارای کاربردهای فراوانی هستند که از آن جمله میتوان به توسعۀ گیم، اپلیکیشنهای تحت وب و غیره اشاره کرد (جهت کسب اطلاعات بیشتر در رابطه با نحوۀ اجرای برنامه به صورت مالتیتِرِد به مقالۀ آشنایی با مفاهیم Thread ،Process و نحوۀ کارکرد Multithreading مراجعه نمایید.) در واقع، زمانی از تِرِدها در پروسهٔ توسعهٔ نرمافزار استفاده میکنیم که بخواهیم برنامهای طراحی کنیم تا در آن بیش از یک کار را به طور همزمان پیش ببریم که برای این منظور نیز کلاسی تحت عنوان Thread در زبان برنامهنویسی جاوا تعبیه شده است.
حال برای درک بهتر مفهوم تِرِدها، مثالی را مد نظر قرار میدهیم بدین صورت که وقتی یک فایل ویدئویی را به صورت آنلاین تماشا میکنیم، نیازی نیست تا صبر کنیم تا فایل کاملاً بارگذاری شود بلکه پخش ویدئو شروع شده و همزمان با نمایش ویدئو بارگذاری بخشهای دیگر آن نیز انجام میشوند. به عبارت دیگر، دو کار نمایش ویدئو و بارگذاری آن به صورت همزمان صورت میگیرند که این موضوع دقیقاً به کاربرد تِرِدها در پیشبرد دو تَسک به صورت همزمان اشاره دارد.
در حقیقت، به طور خلاصه میتوان گفت که با استفاده از تِرِدها میتوانیم نرمافزاری طراحی کنیم تا به جای انتظار برای تکمیل انجام یک کار سپس آغاز کار بعدی، هر دو کار به صورت همزمان پیش روند که اهمیت چنین مسئلهای در توسعهٔ گیم دو چندان میگردد. از سوی دیگر، تِرِدها صرفاً در بازیسازی کاربرد نداشته و در حوزۀ اپلیکیشنهای تحت وب و دسکتاپ نیز کاربردهای فراوانی دارند. به طور مثال، فرض کنیم که یک اپلیکیشن تحت وب با زبان جاوا طراحی کردهایم که در نقش یک وب سرور بوده و وظیفۀ هندل کردن بیش از یک ریکوئست (درخواست) در آنِ واحد را دارا است که این کار به سادگی با استفاده از مفهوم تِرِدها در زبان جاوا امکانپذیر میباشد.
حال در ادامه قصد داریم تا ببینیم که چگونه میتوان به صورت عملی یک تِرِد در زبان برنامهنویسی جاوا ایجاد کرد و اجرای برنامهها با استفاده از چند تِرِد چه مزایایی دارا است. به طور کلی، میتوان گفت که پیادهسازی تِرِدها در زبان برنامهنویسی جاوا به دو روش مختلف امکانپذیر میباشد که عبارتند از:
- روش اول بدین صورت است که کلاسی ایجاد کرده سپس کلاس مذکور ویژگیهای خود را از کلاس
Threadدر زبان جاوا به ارث میبرد. - در روش دوم کلاسی ایجاد کرده و آن را از روی اینترفیسی تحت عنوان
Runnableایمپلیمنت میکنیم که در چنین شرایطی میتوانیم بدون ارثبری از کلاسThreadاقدام به ساخت تِرِد نماییم.
در ادامۀ این آموزش و به منظور درک سازوکار تِرِدها، هر دو روش را مورد بررسی قرار خواهیم داد.
نحوۀ ساخت تِرِد با بهکارگیری کلاس Thread
در این مرحله به بررسی نحوۀ ساخت یک تِرِد با ارثبری از کلاسی تحت عنوان Thread میپردازیم که در ایپیآی زبان جاوا گنجانده شده است. برای شروع، یک پروژۀ جدید تحت عنوان Thread ایجاد کرده و کلاسی به نام ThreadA در آن میسازیم که در ابتدا کدی به صورت زیر خواهیم داشت:
public class ThreadA extends Thread {
}
در کد فوق، با استفاده از کیورد extends کلاس ThreadA را به نحوی پیادهسازی کردهایم تا از کلاس Thread ارثبری نماید و بدین ترتیب کلاس مذکور کلیۀ خصوصیات، ویژگیها و متدهای کلاس Thread را به ارث خواهد برد. حال به منظور درک بهتر مفهوم تِرِدها، کد فوق را بدین صورت تکمیل میکنیم که متدی تحت عنوان ()run از کلاس Thread را اُورراید کرده و در آن سعی میکنیم تا استرینگی را به صورت زیر در کنسول چاپ نماییم:
public class ThreadA extends Thread {
@Override
public void run() {
System.out.println("Thread A");
for (int i = 1; i <= 5; i++) {
System.out.println("From thread A, loop no: " + i);
}
}
}
در کد فوق، با استفاده از دستور Override@ متد ()run از کلاس Thread را اُورراید کردهایم به طوری که متد مذکور در صورت فراخوانی ابتدا استرینگ «Thread A» را در کنسول چاپ میکند که به نوعی مرتبط با آغاز اجرای تِرِد اول میباشد سپس یک حلقۀ for تعریف کردهایم که در آن گفتهایم در هر بار اجرای حلقه مقدار منتسب به متغیر i به همراه استرینگ «:From thread A loop no» در کنسول چاپ شود و این در حالی است که در هر تکرار مقدار منتسب به این متغیر یک واحد افزایش یافته و از سوی دیگر اجرای حلقۀ for نیز تا زمان رسیدن مقدار متغیر i به عدد 5 ادامه مییابد (جهت کسب اطلاعات بیشتر در رابطه با نحوۀ عملکرد حلقههای for به آموزش آشنایی با ساختار حلقۀ for در زبان برنامهنویسی جاوا مراجعه نمایید.)
توجه داشته باشیم که وقتی کلاسی از Thread ارثبری میکند میباید متد ()run از این کلاس را اُورراید کرد چرا که بدین طریق میتوانیم دستورات مورد نظر خود را در آن پیادهسازی نماییم و در غیر این صورت آیدیای اکلیپس واژۀ run را با نقطهچین مشخص مینماید که به منزلۀ وجود اروری مرتبط با متد مذکور در برنامه میباشد.
حال به منظور درک مفهوم مالتیتِرِدینگ و اجرای برنامهها با بهکارگیری چند تِرِد مختلف، علاوه بر کلاس ThreadA کلاس دیگری تحت عنوان ThreadB نیز در پروژهٔ خود ایجاد کرده و در آن متد ()run را به گونهای اُورراید میکنیم تا استرینگی مبنی بر اجرای تِرِد دوم را در کنسول چاپ نماید تا ببینیم اجرای همزمان برنامه توسط دو تِرِد چگونه میتواند باشد. از همین روی، کلاس ThreadB را ساخته و آن را به صورت زیر تکمیل میکنیم:
public class ThreadB extends Thread {
@Override
public void run() {
System.out.println("Thread B");
for (int i = 1; i <= 5; i++) {
System.out.println("From thread B, loop no: " + i);
}
}
}
همانطور که ملاحظه میکنیم، کلاس ThreadB نیز از کلاس Thread ارثبری کرده است و تنها تفاوت آن با کلاس ThreadA این است که در صورت آغاز اجرای تِرِد دوم استرینگی به صورت «Thread B» در کنسول چاپ شده و در ادامه برنامه وارد حلقه شده و استرینگ مرتبط با این کلاس را به صورت «:From thread B, loop no» به همراه مقدار منتسب به متغیر i در کنسول چاپ میکند. به علاوه، توجه داشته باشیم که مقدار متغیر i در هر بار اجرای حلقه یک واحد افزایش یافته و اجرای آن نیز تا زمان رسیدن مقدار متغیر مذکور به عدد 5 ادامه مییابد.
در این مرحله به منظور ساخت آبجکت از روی دو کلاس فوق سپس فراخوانی متدهای ()run روی آبجکتهای ساختهشده، کلاس جدیدی تحت عنوان ActionClass ساخته و در حین ساخت کلاس نیز تیک گزینۀ public static void main را میزنیم که در ابتدا کدی مانند زیر خواهیم داشت:
public class ActionClass {
public static void main(String args[]) {
}
}
در ادامه نیاز به ساخت آبجکت از روی هر دو کلاس ThreadA و ThreadB داریم که برای این منظور کد فوق را به صورت زیر تکمیل میکنیم:
public class ActionClass {
public static void main(String args[]) {
ThreadA a = new ThreadA();
ThreadB b = new ThreadB();
}
}
در کد فوق، مشابه آموزشهای پیشین دو آبجکت تحت عناوین a و b به ترتیب از روی کلاسهای ThreadA و ThreadB ساختهایم. اکنون به هر دو متد ()run از دو کلاس مذکور دسترسی داشته و میتوانیم آنها را روی آبجکتهای مورد نظر فراخوانی نماییم. از سوی دیگر، جهت ایجاد تِرِدها یا به عبارتی اجرای برنامه به صورت مالتیتِرِد نیاز به فراخوانی متدی از پیش تعریفشده از کلاس Thread تحت عنوان ()start داریم که فراخوانی این متد منجر به ایجاد یک تِرِد شده و در ادامه متدهای ()run را فراخوانی مینماید. بنابراین در ادامه متد ()start را روی هر دو آبجکت ساختهشده فراخوانی مینماییم و برای این منظور به روش زیر عمل میکنیم:
public class ActionClass {
public static void main(String args[]) {
ThreadA a = new ThreadA();
a.start();
ThreadB b = new ThreadB();
b.start();
}
}
در کد فوق، ابتدا آبجکتی به نام a از روی کلاس ThreadA ساخته و در ادامه متد ()start را روی آن فراخوانی میکنیم که فراخوانی این متد منجر ایجاد تِرِد اول شده و همچنین متد ()run را جهت اجرا روی تِرِد ساختهشده فراخوانی مینماید. در ادامه، آبجکت جدیدی به نام b از روی کلاس ThreadB ساخته و متد ()start را روی آن فراخوانی نمودهایم که بدین ترتیب تِرِد دیگری ساخته شده و متد ()run از کلاس ThreadB نیز به منظور اجرا روی تِرِد جدید فراخوانی میشود.
اکنون دو تِرِد ایجاد کردهایم که موجب اجرای برنامه به صورت همزمان میشوند بدین معنی که ممکن است تِرِد اول منابع سیستم را به منظور اجرای حلقۀ for از کلاس ThreadA گرفته و آن را یک مرتبه تکرار کرده و در ادامه منابع سیستم به تِرِد دوم اختصاص یافته و حلقۀ for از کلاس ThreadB یک مرتبه اجرا شود و این فرآیند تا اتمام اجرای هر دو حلقه تکرار شود یا به عنوان مثالی دیگر ممکن است تِرِد اول حلقۀ for از کلاس ThreadB را سه مرتبه تکرار کرده و در ادامه منابع سیستم را آزاد کند که در این صورت تِرِد دوم اجرای حلقۀ for از کلاس ThreadA را ادامه داده و آن را یک مرتبه اجرا کند که این فرآیند تا زمان اتمام تعداد اجرای هر دو حلقه تکرار خواهد شد. حال برنامۀ فوق را اجرا کرده و در خروجی خواهیم داشت:
Thread A
Thread B
From thread A, loop no: 1
From thread B, loop no: 1
From thread A, loop no: 2
From thread B, loop no: 2
From thread A, loop no: 3
From thread A, loop no: 4
From thread A, loop no: 5
From thread B, loop no: 3
From thread B, loop no: 4
From thread B, loop no: 5همانطور که میبینیم، ابتدا تِرِدی برای اجرای متد ()run از کلاس ThreadA ایجاد شده و در ادامه تِرِد دیگری برای اجرای متد ()run از کلاس ThreadB ایجاد شده است. در ادامه، تِرِد اول متد ()run از کلاس ThreadA را اجرا کرده و در نتیجه حلقۀ for از این متد یک مرتبه اجرا شده و استرینگ مربوطه در کنسول چاپ گردیده است؛ سپس تِرِد دوم منابع سیستم را در اختیار گرفته و متد ()run از کلاس ThreadB را فراخوانی کرده و حلقۀ for آن را یک مرتبه اجرا نموده و این امر نیز منجر به چاپ استرینگ مربوطه در کنسول شده است.
به همین ترتیب، مجدداً منابع سیستم به تِرِد اول اختصاص یافته و اجرای متد ()run از کلاس ThreadA از سر گرفته شده و بدین ترتیب حلقۀ for برای بار دوم اجرا شده است سپس تِرِد دوم منابع سیستم را در اختیار گرفته و حلقۀ for از کلاس ThreadB را برای بار دوم اجرا کرده است. در مرحلۀ بعدی مجدداً تِرِد اول اقدام به اجرای متد ()run از کلاس ThreadA نموده و دو مرتبه حلقۀ for از این کلاس را اجرا کرده است و به همین منوال مراحل دیگری مشابه آنچه که تاکنون توضیح دادیم تا زمان تکمیل هر پنج مرتبه از هر دو حلقه ادامه یافته و بدین ترتیب حلقههای هر دو کلاس توسط هر یک از دو تِرِد به صورت همزمان اجرا شدهاند. حال اگر برنامۀ فوق را مجدداً اجرا کنیم در خروجی خواهیم داشت:
Thread A
Thread B
From thread A, loop no: 1
From thread A, loop no: 2
From thread A, loop no: 3
From thread A, loop no: 4
From thread B, loop no: 1
From thread A, loop no: 5
From thread B, loop no: 2
From thread B, loop no: 3
From thread B, loop no: 4
From thread B, loop no: 5همانطور که در خروجی میبینیم، در ابتدا دو تِرِد ساخته شده و در ادامه تِرِد اول متد ()run از کلاس ThreadA را اجرا میکند که در این مورد تِرِد اول به تعداد چهار مرتبه حلقۀ for را اجرا کرده و منابع سیستم را آزاد میکند و در ادامه تِرِد دوم با در اختیار داشتن منابع سیستم متد ()run از کلاس ThreadB را اجرا میکند و همانطور که میبینیم این تِرِد یک مرتبه حلقۀ for را اجرا کرده و تِرِد اول مجدداً اجرای حلقۀ for از متد ()run از کلاس ThreadA را از سر گرفته و آن را یک بار دیگر اجرا میکند که در اینجا کار تِرِد اول به پایان رسیده و منابع سیستم را به طور کامل آزاد کرده و در ادامه تِرِد دوم به تعداد چهار مرتبۀ دیگر حلقۀ for از متد ()run از کلاس ThreadB را اجرا کرده و در نهایت اجرای برنامه به پایان میرسد.
نحوۀ ساخت تِرِد با بهکارگیری اینترفیس Runnable
در این بخش از آموزش قصد داریم تا با ایپلیمنت کردن اینترفیسی از پیش تعریفشده تحت عنوان Runnable، در ابتدا یک تِرِد ایجاد کرده و در ادامه به بررسی مفهوم مالتیتِرِدینگ با بهکارگیری این اینترفیس بپردازیم. بنابراین پروژۀ دیگری تحت عنوان Runnable ایجاد کرده و کلاسی به نام ThreadA در آن میسازیم که در ابتدا کدی به صورت زیر خواهیم داشت:
public class ThreadA {
}
به طور کلی، میتوان گفت که در این روش به منظور ایجاد یک تِرِد از اینترفیسی تحت عنوان Runnable استفاده کرده و آن را در کلاس فوق ایمپلیمنت مینماییم. در واقع، همانطور که در ابتدای آموزش اشاره کردیم، با پیادهسازی اینترفیس مذکور در کلاس ThreadA میتوانیم بدون ارثبری از کلاس Thread تِرِدهای مورد نظر به منظور اجرای برنامه را بسازیم که در همین راستا در ادامه اینترفیس Runnable را در کلاس فوق ایمپلیمنت کرده و کد آن را به صورت زیر تکمیل مینماییم:
public class ThreadA implements Runnable {
public void run() {
System.out.println("Thread A");
for (int i = 1; i <= 5; i++) {
System.out.println("From My First Thread, loop no: " + i);
}
}
}در کد فوق، ابتدا نام کلاس را نوشته سپس کیورد implemets و در ادامه نام اینترفیس Runnable را نوشتهایم که بدین ترتیب قصد داریم تا اینترفیس مذکور را در کلاس ThreadA ایمپلیمنت نماییم. بنابراین در ادامه دستورات داخلی متد ()run از اینترفیس Runnable را پیادهسازی مینماییم چرا که متد ()run در اینترفیس Runnable یک متد بدون دستورات داخلی بوده و بدین طریق میتوانیم دستورات مورد نظر خود را در آن پیادهسازی نماییم. در واقع، متد ()run دستوراتی را شامل میشود که قصد داریم تا پس از ساخت تِرِد مورد نظر توسط آن اجرا شوند.
در تفسیر دستورات داخلی متد ()run باید گفت که ابتدا استرینگ «Thread A» به منزلۀ آغاز اجرای تِرِد اول چاپ شده و مشابه مثال پیشین، حلقۀ for به تعداد پنج مرتبه تکرار شده و در هر مرتبه مقدار منتسب به متغیر i به همراه استرینگ «:From My First Thread, loop no» را در کنسول چاپ میکند (توجه داشته باشیم که در هر بار اجرای حلقه مقدار منتسب به متغیر i یک واحد افزایش یافته و اجرای حلقه تا زمان رسیدن مقدار متغیر i به عدد 5 ادامه مییابد.) در این مرحله به منظور اجرای کلاس فوق و ساخت تِرِد، کلاس دیگری تحت عنوان ActionClass ایجاد کرده و در حین ساخت کلاس نیز تیک گزینۀ public static void main را میزنیم که در ابتدا کدی به صورت زیر خواهیم داشت:
public class ActionClass {
public static void main(String[] args) {
}
}
در ادامه، آبجکتی از روی کلاس ThreadA ایجاد کرده و قصد داریم تا با فراخوانی متد ()start روی آبجکت مذکور تِرِدی ساخته و بدین ترتیب متد ()run را با استفاده از آن تِرِد اجرا نماییم که برای این منظور کد فوق را به صورت زیر تکمیل میکنیم:
public class ActionClass {
public static void main(String[] args) {
ThreadA test = new ThreadA();
}
}
در کد فوق، آبجکتی تحت عنوان test از روی کلاس ThreadA ساختهایم و در ادامه به منظور دسترسی به متد ()start و فراخوانی آن از کلاس Thread میباید آبجکتی از روی کلاس Thread ایجاد کرده و در ادامه آبجکت test را به کانستراکتور این کلاس پاس دهیم که بدین ترتیب در ادامه میتوانیم متد ()start را روی آبجکت جدید ساختهشده از روی کلاس Thread فراخوانی نماییم. بنابراین به منظور ساخت آبجکت جدید از روی کلاس Thread کد فوق را به صورت زیر تکمیل مینماییم:
public class ActionClass {
public static void main(String[] args) {
ThreadA test = new ThreadA();
Thread thread = new Thread(test);
thread.start();
}
}
همانطور که ملاحظه میکنیم، آبجتی به نام thread از روی کلاس Thread ساخت و آبجکت test را داخل علائم ( ) در سطر چهارم درج کرده و بدین طریق آن را به کانستراکتور کلاس Thread پاس دادهایم که در این صورت آبجکت جدیدی به نام thread از روی کلاس Thread ساخته میشود و در ادامه متد ()start از این کلاس را روی آبجکت جدید ساختهشده فراخوانی مینماییم. حال برنامۀ فوق را اجرا کرده و در خروجی خواهیم داشت:
Thread A
From My First Thread, loop no: 1
From My First Thread, loop no: 2
From My First Thread, loop no: 3
From My First Thread, loop no: 4
From My First Thread, loop no: 5همانطور که میبینیم، تِرِد مورد نظر ایجاد شده و متد ()run را اجرا کرده و منجر به چاپ استرینگهای تعریفشده داخل حلقۀ for گردیده است به طوری که دستورات داخلی حلقۀ for پنج مرتبه تکرار شده و در نهایت اجرای برنامه متوقف شده است.
حال اگر بخواهیم تا نحوۀ اجرای برنامه را به صورت مالتیتِرِد مورد بررسی قرار دهیم، نیاز است تا کلاس دیگری تحت عنوان ThreadB ایجاد کرده و بدین ترتیب تِرِد دومی ساخته و برنامۀ فوق را با دو تِرِد اجرا نماییم. بنابراین کلاس دیگری به نام ThreadB داخل پروژهٔ خود ایجاد کرده و در ابتدا کدی به صورت زیر خواهیم داشت:
public class ThreadB implements Runnable {
}در ادامه، کلاس فوق را از روی اینترفیس Runnable ایمپلیمنت کرده و متد ()run را در آن پیادهسازی مینماییم که برای این منظور کد فوق را به صورت زیر تکمیل میکنیم:
public class ThreadB implements Runnable {
public void run() {
System.out.println("Thread B");
for (int i = 1; i <= 5; i++) {
System.out.println("From My Second Thread, loop no: " + i);
}
}
}همانطور که در کد فوق میبینیم، مشابه کلاس ThreadA متد ()run را به گونهای پیادهسازی نمودهایم که در صورت فراخوانی ابتدا استرینگ «Thread B» مبنی بر آغاز اجرای برنامه توسطر تِرِد دوم چاپ شده و در ادامه حلقۀ for اجرا میشود که بدین ترتیب استرینگ «:From My Second Thread, loop no» به همراه مقدار منتسب به متغیر i پنج مرتبه در کنسول چاپ میگردد به طوری که در هر مرتبه اجرای آن نیز یک واحد به مقدار i افزوده میشود تا در صورت رسیدن این مقدار به عدد 5 اجرای حلقه متوقف گردد. حال به منظور اجرای کلاس فوق و ساخت تِرِد دوم به کلاس ActionClass بازگشته و مشابه کلاس ThreadA آبجکتی از روی کلاس ThreadB به صورت زیر میسازیم:
public class ActionClass {
public static void main(String[] args) {
ThreadA test = new ThreadA();
Thread thread = new Thread(test);
thread.start();
// Second Object
ThreadB testB = new ThreadB();
Thread threadB = new Thread(testB);
threadB.start();
}
}همانطور که در کد فوق میبینیم، ابتدا آبجکتی تحت عنوان testB از روی کلاس ThreadB ساخته و در ادامه به منظور ساخت آبجکتی از روی کلاس Thread آبجکت testB را به کانستراکتور کلاس Thread پاس میدهیم و بدین ترتیب به متد ()start از کلاس Thread دسترسی داشته و آن را روی آبجکت جدید ساختهشده فراخوانی مینماییم که در نتیجۀ فراخوانی این متد، تِرِد دیگری ساخته شده و متد ()run از کلاس ThreadB فراخوانی میگردد به طوری که با اجرای برنامۀ فوق در خروجی خواهیم داشت:
Thread A
Thread B
From My First Thread, loop no: 1
From My Second Thread, loop no: 1
From My Second Thread, loop no: 2
From My Second Thread, loop no: 3
From My First Thread, loop no: 2
From My Second Thread, loop no: 4
From My First Thread, loop no: 3
From My Second Thread, loop no: 5
From My First Thread, loop no: 4
From My First Thread, loop no: 5
مشابه روش پیشین در این مورد نیز برنامه به صورت مالتیتِرِد اجرا شده است به طوری که در ابتدا هر دو تِرِد ایجاد شده و در ادامه حلقۀ for از کلاس ThreadA یک مرتبه توسط تِرِد اول اجرا شده سپس تِرِد دوم حلقۀ for از کلاس ThreadB را سه مرتبه اجرا میکند. در ادامه، تِرِد اول با در اختیار داشتن منابع سیستم اجرای حلقۀ for از کلاس ThreadA را از سر گرفته و یک مرتبۀ دیگر آن را اجرا کرده سپس تِرِد دوم منابع سیستم را در اختیار گرفته و برای بار چهارم حلقۀ for از کلاس ThreadB را اجرا مینماید و این مراحل تا زمانی ادامه مییابد که هر دو حلقۀ for از کلاسهای مذکور پنج مرتبه تکرار شده و استرینگهای مربوطه را در کنسول نمایش دهند.
در این آموزش به بررسی کاربرد تِرِدها در اجرای برنامه پرداخته و نحوۀ ایجاد یک تِرِد با بهکارگیری دو روش مختلف در زبان برنامهنویسی جاوا را آموختیم و دیدیم که چگونه میتوان با استفاده از هر یک از روشهای مذکور برنامهای را پیادهسازی کرد که به صورت مالتیتِرِد اجرا شود.
