סקירה כללית
thread Aהוא קיצור של thread of control . נלמד גם מונחים נוספים בהמשך הסקירה.
אם אי פעם עבדת עם דפדפן רשת, השתמשת ב- threads מבלי שאפילו היית מודע לכך.
דפדפני הרשת משתמשים ב- threads שונים הפועלים במקביל לביצוע מגוון משימות, לכאורה בו זמנית.
לדוגמא בדפי אינטרנט רבים, אתה יכול לגלול באמצעות העכבר ולקרוא את הטקסט, לפני שכל הסימנים מופיעים על המסך.
במקרה זה הדפדפן מוריד סימן ב thread אחד ותומך בפעולת הגלילה שלך ב- thread אחר.
C ו- ++C לא תומכות בפעולת ה- threading כפעולה המוטמעת בשפה. אם אתה צריך לכתוב תוכנית עם threads באחת השפות האלה, עליך לטפל במאפיינים של מערכת ההפעלה או אולי להרכיב סדרה של ספריות מחלקה הבנויות במיוחד כדי לתמוך ב- threading עבור פלטפורמה מסוימת. |
ג'אווה תומכת ב- threading ישירות, כך שהיא מאפשרת לכתוב תוכניות threads בלתי תלויות בפלטפורמה (לצערנו, עם מס' הבדלים בהתנהגות של הפלטפורמות השונות).
רוב הספרים מקדישים עמוד אחד או יותר לנושא זה. דיון מעניין, ומשעשע במידה מסוימת, בנושא אפשר למצוא בספר Java Primer Plus מאת Tyma, Torok ו-Downing.
בקצרה ניתן לומר כי multiprocessing מתייחס לשתי תוכניות או יותר הפועלות "לכאורה" במקביל, תחת בקרה של מערכת ההפעלה. לא חייב להיות שום קשר בין התוכניות מלבד העובדה שאתה רוצה להתחיל ולהריץ את כולן בו זמנית.
ל- multiprocessing מתייחסים לפעמים כprocess "כבד", כנראה מכיוון שהאתגרים המעורבים בהפעלתו הם מורכבים. המונח Multithreading פירושו שני tasksאו יותר הפועלים "לכאורה" בו זמנית במסגרת אותה התוכנית. לעיתים קרובות מתייחסים אליו כאל "process קל" אולי כי האתגרים הכרוכים בהפעלתו הם…, נו, בטח תפסת כבר את הרעיון… |
בשני המקרים השתמשתי בצירוף "לכאורה בו-זמנית" מכיוון שאם לתוכנית יש מעבד בודד,
ה processes לא ממש רצים בו-זמנית אלא משתמשים באותו מעבד על בסיס מסוים.
על כל פנים, במקרה של פלטפורמות בעלות כמה מעבדים יתכן שה processes אכן ירוצו בו-זמנית.
בשני המקרים, התנהגות מערכת ההפעלה יוצרת את הרושם של הרצה בו-זמנית.
חשוב מזה, multithreading יכול לייצר תוכניות שיבצעו יותר עבודה באותו פרק זמן,
לאור העובדה שהמעבד משמש לכמה tasks.
Multiprocessing מוטמע במערכת ההפעלה.
כל שתי תוכניות או יותר הניתנות להרצה בפלטפורמה, ניתן להריץ על בסיס של multiprocessing מבלי שכותבי התוכנית יצטרכו לתכנן זאת מראש.
לעומת זאת, למרות ש- multithreading בהחלט מחייב תמיכה מצד מערכת ההפעלה, בשורה התחתונה, הוא מוטמע בתוך התוכנה.
יותר מזה, תכנון מוקדם נדרש מצדו של כותב התוכנית, מכיוון שיש צורך שהיא תהיה בנויה במיוחד עבור הרצה בשיטת ה multithreading .
ולבסוף, שתי תוכניות multithreading או יותר ניתן להריץ בו-זמנית במערכת של multiprocessing. לכן, multithreading ו- multiprocessing יכולים להתקיים בו-זמנית זה לצד זה.
למה אנו זקוקים ל- multithreading?
רוב הספרים מקדישים גם לסוגייה זו עמוד אחד או יותר.
שוב, בתמצית, תוכניות מסוגים רבים יכולים לעשות שימוש באופן יעיל יותר במשאבים מכניים זמינים על ידי הפרדת המשימות ל threads שיפעלו בו-זמנית.
בחלק מה tasks יש פרקי זמן ארוכים שבהם לא מתבצעת שום פעולה תכליתית, בזמן שאחרות זקוקות באופן תמידי לכל המשאבים הקיימים בסביבה.
נניח לדוגמא שהיינו צריכים תוכנית שתוכל לקרוא 1000 מספרים מהמקלדת, תחשב את השורש של כל מספר ברמת דיוק של מאה מקומות אחרי הנקודה, ותעתיק את התוצאות לקובץ על התקליטור.
רוב הפעולות הקשורות בקלט של המקלדת צורכות מעט מאוד משאבים של המחשב.
בתוכנה סדרתית טהורה, כמו שרגילים לראות ב- ,Pascal C , ו ++C , חלק גדול מהזמן המחשב יבזבז בהמתנה לכך שהאופרטור של המקלדת יקיש על מקש ה- enter ורק בחלק קטן מהזמן הוא למעשה יריץ נתונים מהבאפר של המקלדת.
באופן דומה, אם כי הרבה פחות חריף, גם תוכנית שצריכה להעתיק נתונים לקובץ תקליטור מבזבזת זמן, כשמערכת ההפעלה מאתרת את הקובץ, ממקמת את הראש-כותב וכו' , לפני שבכלל מועברים נתונים כלשהם.
אם כך, חלק גדול ממשאבי המחשב מתבזבז על קלט/פלט (input/output)של פעולות המקלדת ופעולות אחרות.
אני מעולם לא ניסיתי לכתוב תוכנית שתוכל לחשב את השורש של מספר ברמת דיוק של מאה מקומות אחרי הנקודה , בכל אופן, אני בטוח שהיא תצרוך את כל משאבי המחשב שביכולתה לאגור, לפחות בפרצים קצרים (short bursts).
אם היינו כותבים את התוכנית שלנו כך ש:
- הקלט של המקלדת יטופל ע"י אחד מה threads
- פלט של התקליטור יטופל ע"י thread שני
- והחישוב יטופל ע"י thread שלישי
ל- thread שיטפל בחישוב תהיה גישה לרוב משאבי המחשב בזמן ששני ה threads האחרים יהיו 'חסומים' בהמתנה לתקשורת עם המקלדת ועם הבקר (controller) של התקליטור.
המונח 'חסום' פירושו שה- thread מחכה שמשהו יקרה ולא משתמש בינתיים במשאבים של המחשב.
לתוכניות סדרתיות אין את היתרון הזה. בעוד שניתן לכתוב תוכניות שרצות במצומצם של לולאות בקרה, בוחנות מגוון מצבים ומכניסותtasks לתורים, זו כנראה שיטה פחות יעילה מאשר threading.
מובן שבאמצעותthreading כל מה שהשגנו הוא העברה של ה processes שחוזרים על עצמם מהאפליקציה למערכת ההפעלה, אבל נוכל לקוות שמתכנתי המערכת יוכלו לעשות עבודה טובה יותר משלנו במימוש process כמו זה.
בין אם הוא טוב יותר ובין אם לא, צריך לתכנת אותו רק פעם אחת - במהלך הפיתוח של מערכת ההפעלה, ולא לתכנת כל פעם מחדש בדרגות שונות של יעילות עבור כל תוכנת יישום חדשה.
שתי דרכים לבצע threading
יש שתי דרכים בג'אווה ליצור תוכניות בעלות threads:
- ליישם את הממשק הניתן להרצה (Runnable interface)
- לירוש מהמחלקה Thread
אתחיל בכך שאציג לכם דוגמא פשוטה עבור כל אחת מהשיטות. יש כותבי תוכניות הגורסים כי השיטה הראשונה היא היותר מונחית-עצמים מבין השתיים.
בין אם זה נכון ובין אם לא, זו שיטה שבה ניתן כנראה להשתמש בכל המקרים, בעוד שבשיטה השנייה לא ניתן להשתמש במצבים מסוימים (ועל כך ארחיב מאוחר יותר).
יישום הממשק הניתן להרצה
כל מה שנידרש כדי 'לייצר' thread בג'אווה הוא:
- ליצור מופעים לאובייקט מסוג Thread
- להפעיל את מתודת ה()run שלו
הקוד שישיג את המשימה המתבקשת מה thread (לבצע את העבודה) נמצא במתודה run() או במתודות אחרות המופעלות על ידה.
אחת הדרכים ליצור מופעים לאובייקט מהמחלקה Thread (שתהיה הנושא של אחד מהפרקים הבאים) היא:
· לרשת ממחלקת ה-Thread לתוך מחלקה משלך.
אחר כך, דרוס את המתודה run() של המחלקהThread במחלקה החדשה שלך.
חשוב: כדי להריץ של thread למעשה, אתה לא מפעיל את המתודהrun() על האובייקט שלך, אלא את המתודה start(). |
עם זאת, לפעמים אי אפשר לירוש מהמחלקה Thread
מכיוון שאתה חייב לרשת ממחלקה אחרת כל שהיא. זכור כי ג'אווה אינה תומכת בירושה מרובה. בתיכנות של Applets , לדוגמא, אתה חייב לירוש מהמחלקה Applet.
במקרים בהם לא ניתן לירוש מהמחלקה Thread , אתה יכול לבנות את המחלקה שלך כך שהיא תירש מכל מחלקה רצויה אחרת , וגם כך שהיא תיישם את הממשק הניתן להרצה.
אם כך, הדרך השנייה (שהיא הנושא של פרק זה) לעשות מופעים לאובייקט של המחלקה Thread היא:
- ממש את הממשק הניתן להרצה במחלקה שלך (תוך ירושה ממחלקה אחרת אם יש צורך)
- לאחר מכן, צור מופעים לאובייקט מהמחלקה שלך
כמו מקודם, אתה צריך לדרוס את המתודהrun() של המחלקה Thread במחלקה החדשה שלך. גישה זו מומחשת בתוכנית ההדגמה
כפי שתוכל לראות, תוכנית זו מגדירה מחלקה חדשה המכונה MyThread היורשת מהמחלקה המכונה DoNothing וממשת את הממשק הניתן להרצה.
אנו דורסים את המתוד run() המחלקה הנקראת MyThread כדי להציג מידע על ה thread שמומש על ידי אובייקט ממחלקת MyThread.
הסיבה היחידה לירוש ממחלקת DoNothing בדוגמא פשוטה זו היא השימוש בנתיב הירושה האפשרי היחיד למחלקה של ג'אווה.
המטרה היא לדמות מצב שבו אתה חייב לירוש ממחלקה אחרת כל שהיא.
ירושה ממחלקה אחרת היא אינה חובה, וגישה זו פועלת באותה מידה של יעילות גם כשאתה לא יורש מאף מחלקה אחרת.
הקוד בmain() יוצר מופעים לשני אובייקטים מסוגThread תוך שימוש באחת מהפונקציות הבונות הזמינות משלThread כפי שנראה להלן:
Thread myThreadA = new Thread(new MyThread(),"threadA"); |
הפונקציה הבונה המסוימת הזו דורשת שני פרמטרים: הראשון הוא אובייקט ממחלקה כלשהי הממשת את הממשק הניתן להרצה. הפרמטר השני, הוא מחרוזת הנותנת שם ספציפי ל- thread. (שים לב שהשם הניתן ל thread אינו תלוי בשם של משתנה-ההפניה המפנה לאובייקט ה thread). קיימות מגוון מתודות לתמרון מצב ה thread על ידי התייחסות שמית אליו.
קוד נוסף ב main() מתחיל את הרצת שני ה threads על ידי הפעלת המתודהstart() על כל אחד מהאובייקטים של ה thread.
הthreads נעצרים והתוכנית מסתיימת כש ל- threads אין עוד מה לעשות.
(זה לא מה שקרה ב JDK 1.0.2 ).
דבר נוסף שצריך לשים לב אליו, הוא ש main הוא thread בפני עצמו, המופעל על ידי
המתרגם (interpreter) .
קוד במתודה run() גורם לכך שיוצגו השמות של כל אחד מה-threads (יחד עם מידע נוסף)
לשני ה threads שיצרו להם מופעים ושהופעלו בתוכנית.
אם הכנסת קוד ב main() גורמת להצגת מידע דומה לגבי ה- threadשל main.
המתודה sleep() מופעלת על ה thread של main לביצוע השהייה של שניה אחת.
במתודה sleep() ובמתודות נוספות שלThread , נדון במהלך הפרקים הבאים.
תוכנית זו מדגימה אחת מתוך שתי גישות אלטרנטיביות ליצירת מופעים ולהרצת threads בתוכנית ג'אווה.
זו הגישה שחובה לאמץ בכל פעם שהמחלקה בה משתמשים כדי ליצירת מופעים לאובייקט של
הthread צריכה לירוש ממחלקה אחרת כל שהיא. כפי שציינו קודם, גישה זו נכונה לא פחות גם למקרים שבהם לא נחוץ לירוש מחלקה אחרת.
זו הסקירה הכללית על שתי הגישות בהן דנו.
הגישה הבאה יכולה להיות יעילה כשהמחלקה בה משתמשים ליצירת מופעים לאובייקט ה thread אינה נדרשת לירוש ממחלקה אחרת כל שהיא, כך שהיא יכולה לירוש מהמחלקה Thread.
ירושה מהמחלקה Thread
הבה נביט באותה תוכנית פשוטה שיורשת ממהמחלקה Thread ולא מיישמת את הממשק הניתן להרצה (Runnable interface).
בשיטה זו, המחלקה החדשה המכונה MyThread יורשת מהמחלקה Thread ולא מיישמת את הממשק הניתן להרצה באופן ישיר.
(המחלקהThread מיישמת את Runnable כך ש- MyClass למעשה מממשת את Runnable באופן עקיף).
בשאר המובנים, תוכנית זו פועלת כעקרון באופן דומה לתוכנית הקודמת.
גישה אחרת לירושה מהמחלקה Thread
התוכנית הבאה גם היא יורשת מהמחלקהThread אלא שהיא במידה מסוימת פשוטה יותר מהקודמת. בדוק אם אתה מסוגל להצביע על השוני.
רמז: בחן בקפידה את שמות
הthreads הבודדים ושים לב כי יש להם שמות של ברירת מחדל. תוכנית זו משתמשת בגרסה אחרת של הפונקציות הבונות של Thread מצריכה שום ארגומנטים. לכן, הפקודות ליצירת המופעים לאובייקטים של Thread הן קצת פחות מורכבות. מלבד זאת, ביסודה היא די דומה לתוכנית הקודמת. זה התחביר שאתה צפוי לראות ברוב המקרים בספרים ובמאמרים שעוסקים ב multithreading.
עצירת threads
כפי שכבר הזכרנו, ה threads נעצרים והתוכנית מסתיימת כשלכל הthreads אין יותר מה לעשות. זה לא היה המצב ב JDK 1.0.2,
ולעיתים היה צורך להפעיל את מתודות הstop() כדי לגרום לthread לעצור.
Daemon Threads
לפי הספר Java Primer Plus, אם אתה מגדירthread כ- daemon thread על ידי שימוש במתודה setDaemon() אתה קובע באופן מפורש שהthread שייך למערכת ולא
ל- process שייצר אותו. Daemon threads יעילים למקרה שאתה מעוניין שה thread ירוץ ברקע לפרקי זמן ממושכים. לפי Deital & Deital בספרם:
"Java, How to Program" ההגדרה של daemon thread
07-12-03 / 20:07 עודכן , 04-10-03 / 15:22 נוצר ע"י רונית רייכמן בתאריך