ראשי
פרק 1
פרק 2
פרק 3
פרק 4
פרק 5
פרק 6
פרק 7
פרק 8

פרק 8 - תהליכונים (threads) וריבוי משימות


תוכן הפרק:

תהליכונים
יצירת תהליכון
עדיפויות
סנכרון תהליכונים
קבוצות תהליכונים



תהליכונים

תהליכון, thread, הוא קטע קוד מוגדר, אשר יבוצע בנפרד מה"זרימה" הרגילה של התוכנית, בצורה מקבילית. כל תוכנית בג'אווה תרוץ לפחות בתור תהליכון בודד, שיווצר ע"י המכונה הוירטואלית.

עיבוד מקבילי?

קל לראות כי המון זמן מחשב מתבזבז בהמתנה לסיום פעולות שדורשות הרבה זמן. ישנן כמה פעולות שכיחות, כמו קלט - אשר מחכה לתגובה ה"איטית" של המשתמש האנושי, פלט - אשר מחכה לאישור המדפסת, פניה לדיסק הקשיח וכו' שכאשר פונים אליהן, למעשה המעבד לא מנוצל באותו זמן, ותוכניות אחרות שהיו יכולות לרוץ מחכות עד לסיום הפעולות היקרות בזמן.
כדי להתגבר על הבעיה משתמשים בעיבוד מקבילי. כלומר, כמה תוכניות ישתמשו לסרוגין ביכולת העיבוד של המחשב וכך הניצולת של המחשב תגדל.
כמובן שאנו מדברים על עיבוד כמו-מקבילי, ולא מקבילי באמת. עיבוד מקבילי אמיתי נעשה ע"י מספר מעבדים / מכונות, וגם לו יש יתרונות עצומים משל עצמו. בכל אופן, אנו נתרכז במערכות חד-מעבדיות, כמו שלרובנו יש בבית.
כמו כן, לא נרחיב כאן את הדיבור על יתרונות וחסרונות העיבוד המקבילי. לשם כך נצטרך קורס שלם. נסתפק במה שהזכרנו בקצרה.

בכל אופן, הפתרון בג'אווה לריבוי משימות מבוקר הוא שימוש בתהליכונים - threads.

אז מהו תהליכון?

תהליכון - יחידת קוד תוכנית לביצוע, מצביע להוראה הנוכחית בקוד (ip), מחסנית וערמה.

  • קטע הקוד - פונקציה מסוימת וכל הפונקציות להן היא קוראת.
  • מחסנית הקריאות - מסגרות הפונקציות שנקראו רקורסיבית ע"י פונקציית התהליכון.
  • ערמה (Heap) - שטח זיכרון, שבו מוקצים העצמים שמוגדרים בקטע הקוד שבתהליכון.
  • מצביע להוראה (ip) - מצביע להוראה הבאה שאמורה להתבצע בקוד.

המחלקה Thread היא מחלקה שממשת את הממשק Runnable:

Thread ו-Runnable

כל תהליכון מקבל פרק זמן מסוים לריצה (קואנטום), שבתומו ה-JVM תעבור לתהליכון הבא בתור.
מערכת ג'אווה תתן זכות קדימה לתהליכון עם עדיפות גבוהה. במקרה של עדיפויות שוות ההחלטה תהיה בידי מערכת ההפעלה. כדי למנוע מצב של הרעבה של התהליך, גם תהליך בעדיפות נמוכה יקבל, מדי פעם, "פרוסת" זמן עיבוד.
כל תהליך שנוצר עובר למצב ריצה עם הפעלת פונקציית ( )start שלו ואז הוא נכנס לתור ההמתנה. יש באפשרותו להשעות את עצמו, ע"י שימוש בפונקציית ( )yield (שמוותרת על המשך פרק הזמן שהוקצה לתהליך למען תהליך בעדיפות זהה), או בפונקציית ( )sleep (שקובעת זמן המתנה במילי-שניות). סיום הריצה מתבצע או כשהתהליך הסתיים, או בסיום יזום, ע"י קריאה לפונקציה ( )stop, או בהצבת ערך null לתהליכון.
שימוש בפונקציה ( )isAlive יחזיר את מצב התהליכון, אם הוא חי או לא.


למעלה

יצירת תהליכון

קיימות שתי דרכים ליצירת תהליכון:

  1. הגדרת מחלקה היורשת מ-Thread, ומימוש הפונקציה ( )run.
  2. הגדרת מחלקה כמממשת לממשק Runnable, מימוש הפונקציה ( )run ויצירת עצם Thread עוטף.

דוגמא לשיטה הראשונה:

נבנה מחלקה, שתיצור שני תהליכונים, מסוג שניצור בהמשך, ונפעיל את שני התהליכונים ע"י פונקציית start.

מחלקת Thr - המחלקה הראשית

נבנה את מחלקת התהליכון. פונקציית הקונסטרקטור תתן שם לתהליכון. התהליכון עצמו יריץ לולאה 10 פעמים, כשבתוכה הוא "ינוח" זמן אקראי כלשהו, ע"י פקודת sleep. בזמן שתהליכון אחד ישן המעבד יפנה לעבד את התהליכון השני.

מחלקת MyThread - מחלקת התהליכון

פלט אפשרי:

פלט אפשרי


דוגמא לשיטה השניה:

המחלקה הראשית:

מחלקת RunThr - המחלקה הראשית
המחלקה המממשת את Runnable:
מחלקת MyRunThread - מחלקת התהליכון

ההבדלים בין שתי השיטות

בתוכנית הראשונה מחלקת MyThread יורשת את Thread, ולכן כל שנדרש לעשות הוא ליצור במחלקה הראשית, Thr, עצם מסוג MyThread ולהפעיל אותו.
בתוכנית השניה, מאחר ו-MyRunThread אינה יורשת מ-Thread, אלא רק מממשת את Runnable, כדי ליצור עצם מסוג Thread (כלומר, כדי ליצור תהליכון חדש) צריך ליצור עצם מסוג MyRunThread, ואותו ולהעביר אותו כפרמטר לעצם מסוג Thread.
במחלקת MyRunThread עצמה: מאחר והמחלקה לא יורשת מ-Thread, לא קראנו לקונסטרקטור האבא, אלא יצרנו את המחרוזת name בצורה ישירה. בנוסף, קראנו לפונקציה ( )Thread.sleep בשמה המלא, ולא רק ל-sleep.


למעלה

עדיפויות

ע"י הפונקציה ( )setPriority ניתן לקבוע את רמת העדיפות של התהליכון.
קיימים שלושה סוגי רמות עדיפות עיקריים:

MAX_PRIORITY
NORM_PRIORITY
MIN_PRIORITY

תהליכון שנוצר ע"י המכונה המדומה יהיה בעל עדיפות נורמלית. כמו כן, כל תהליכון שיווצר ממנו יורש את אותה עדיפות, אלא אם כן נשנה אותה.
לדוגמא נוסיף עדיפות בתוכנית שכתבנו:

המחלקה הראשית:
מחלקת PriThr - המחלקה הראשית
מחלקת התהליכון:
מחלקת MyPriThread - מחלקת התהליכון
פלט אפשרי לתוכנית:
פלט אפשרי

שינינו את זמן ההמתנה מפונקצית שינה (sleep), שבה התהליכון השני היה מקבל זמן עיבוד בזמן שינת התהליכון הראשון, ללולאה שמתבצעת במעבד ושבה לא עובר זמן העיבוד לתהליכון אחר. התהליכון השני יכול היה לקבל זמן עיבוד באמצע עיבוד התהליכון הראשון, במקרה שהלולאה היתה גוזלת זמן מוגדר גדול, ע"מ למנוע הרעבה.


למעלה

סנכרון תהליכונים

בדרך כלל, עבודה בתהליכונים תעשה בצורה מתואמת ביניהם. למשל, תהליכון אחד שולח הודעות והתהליכון השני מקבל אותן. למצב כזה דרוש סנכרון בין התהליכונים.
סנכרון כזה יעשה ע"י שימוש בפונקציות ( )wait ו-( )notify ובמנגנון synchronized.

  • ( )wait - תגרום לתהליכון לחכות. התהליכון יכנס לתור הממתינים ותהליכון אחר יתפוס את מקומו.
  • ( )notify - קריאה מהתהליכון שמתבצע לתהליכון אחר, שממתין בתור, להכנס לפעולה (( )notifyAll יעיר כמה תהליכונים שממתינים).

מנגנון synchronized

זהו מנגנון המאפשר לנעול קטע קוד (עצם, פונקציה בודדת, או קטע כלשהו), כך שרק תהליכון אחד יוכל להשתמש בו באותו הזמן, וכך נפתרת בעיית עיבוד מקבילי ידועה - בעיית הקטע הקריטי.
דוגמא:

מחלקת Message, מחלקה כללית לשליחת מסר, הכוללת: ערך של המסר ודגל, שמראה אם תיבת המסרים מלאה או ריקה. בנוסף - שתי פונקציות: שליחת מסר - שמקבלת ערך של מסר ושולחת אותו אם תיבת המסרים ריקה (דגל שלילי), או מחכה למשיכת המסר הקודם ורק אז תשלח את החדש; קבלת מסר - מחכה עד שיש משהו בתיבת המסרים, ואז קוראת אותו. שתי הפונקציות מוגנות ע"י מנגנון הסנכרון כך שלא יאבדו את הפיקוח באמצע הפעולה.  
מחלקת Message
מחלקת Send, המשמשת לשליחת ההודעות:
מחלקת Send
מחלקת Get, אשר מקבלת את ההודעות:
מחלקת Get
מחלקת Useprog - המחלקה הראשית:
מחלקת Useprog - המחלקה הראשית
והתוצאה:
פלט אפשרי

התוכנית יצרה שני תהליכונים, שהעבירו ביניהם עצמים מסוג Message בצורה מסונכרנת.

עוד על תהליכונים:

  • אם נגדיר זמן ל-( )wait, התהליכון ימשיך גם ללא הפונקציה ( )notify.
  • אפשר לחטוף זמן מהתהליכון, ע"י הפרעה - ( )interrupt - שתגרום לזריקת חריגת InterruptedException.
  • ניתן להשעות תהליכון ע"י קריאה לפונקציית ההשעייה - ( )suspend - שלו ולהחזירו לפעולה ע"י פונקציית ההפעלה מחדש - ( )resume.
  • תהליכון יחכה לתהליכון אחר בעזרת פונקציית ( )join של התהליכון השני.

למעלה

קבוצות תהליכונים

כל תהליכון שייך לקבוצה כלשהי של תהליכונים, ThreadGroup. ניתן לשתף מאפיינים שונים בין כל התהליכונים השייכים לקבוצה. למשל:

  • מצב daemon. ניתן להגדיר את התהליכונים בקבוצה שירוצו ברקע. המכונה המדומה תיסגר לאחר שכל התהליכונים שאינם במצב זה יסיימו את פעולתם, כלומר, קבוצות התהליכונים שכן במצב daemon ימשיכו לרוץ ברקע. הפונקציה (setDaemon(false/true תקבע את מצב הקבוצה, והפונקציה ( )isDaemon תבדוק זאת.
  • ניתן להפעיל פונקציות כמו ( )stop( ), suspend( ), resume על כל הקבוצה ביחד.

יצירת קבוצה תעשה ע"י המחלקה ThreadGroup.
דוגמא:

;("ThreadGroup tg = new ThreadGroup("tg1
;("Thread t = new Thread(tg, "thread 1

יצרנו קבוצה ושייכנו אליה תהליכון בעזרת הקונסטרקטור של Thread.

במחלקת Thread קיימים כמה קונסטרקטורים המותאמים לקבוצות:

(public Thread (ThreadGroup group, Runnable target
(public Thread (ThreadGroup group, String name
(public Thread (ThreadGroup group, Runnable target, String name

פונקציות על קבוצת תהליכונים

  • ( )activeCount - מספר התהליכונים הפעילים בקבוצה.
  • ( )activeGroupCount - מספר תתי הקבוצות הפעילים שבקבוצה.
  • ( [ ]enumerate(Thread - מחזירה את מספר התהליכונים שבקבוצה כמערך.
  • ( )getParent - קבלת קבוצת האם של הקבוצה.



למעלה