» נושאי לימוד
» נושאי לימוד
יום שני 29 באפריל 2024
סנכרון של threads
דף ראשי  מתקדמים  Treads of control  פרטים טכניים  סנכרון של threads גרסה להדפסה

סנכרון של threads

עד לשלב זה, הנחנו שכל ה threads  רצו באופן בלתי מסונכרן (asynchronously), ובאופן בלתי תלוי באחרים - אלא שלעיתים קרובות זה לא המצב.

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

 

 הגינות, הרעבה, וקיפאון (Fairness, Starvation, and Deadlock)

לדברי Campione & Walrath "אם אתה כותב תוכנית שבה מספר threads בו זמניים מתחרים על משאבים, חובה עליך לנקוט באמצעי זהירות כדי להבטיח הגינות. המערכת הוגנת כשכל thread  מקבל גישה מספקת למשאב מוגבל בכדי להתקדם באופן משמעותי."

עוד הם מוסיפים: "מערכת הוגנת מונעת 'הרעבה' (starvation) ו'קיפאון' (deadlock). הרעבה מתרחשת כשאחד מה threads , או יותר, בתוכנית שלך נחסם  מלקבל גישה למשאב ולכן אינו יכול להתקדם. קיפאון הוא הצורה האולטימטיבית של הרעבה. הוא מתרחש כששני threads  או יותר ממתינים במצב שאי אפשר לענות על צרכיו. המצב השכיח ביותר לקיפאון הוא כשכל אחד משני threads (או יותר) מחכה לשני שיעשה משהו. "

 

מודל היצרן / צרכן (The Producer/ Consumer Model)

כמעט כל ספר על תכנות בג'אווה מסביר את השימוש במתודות מסונכרנות על ידי תוכנית שמיישמת את מודל היצרן/צרכן. במודל הזה thread  אחד הוא יצרן של נתונים בעוד שthread  אחר הוא צרכן של אותם נתונים.

ישנו אזור משותף של מאגר נתונים שבו שם היצרן את הנתונים והצרכן מקבל אותם. בכדי למנוע בעיות, חובה למנוע מהצרכן מלנסות לקבל מידע מהאזור המשותף של מאגר הנתונים בזמן שהיצרן מביא נתונים לאזור המאגר.   

כמו כן יש למנוע מהיצרן מלנסות לשים מידע באזור מאגר הנתונים המשותף בזמן שהצרכן מקבל משם מידע. נלך בעקבות גדולים ממנו, ונמחיש  סנכרון של threads על ידי הצגת תוכנית הדגמה שמיישמת את מודל הצרכן/יצרן.

תוכנית ההדגמה שלנו תשתמש בתור FIFO רגיל כאזור מאגר הנתונים המשותף. למרות שקוד המקור לתור נתון, אנו נניח שהוא מובן באופן כללי על ידי כולם, ולא נקדיש זמן לדון בכך, מלבד הקביעה כי תור הוא מבנה נתונים שמקובל להשתמש בו בעיבוד נתונים כשהאלמנט הנתון הראשון שמכניסים לתוך המבנה הוא גם הראשון שיצא ממנו.

ספרי תיכנות רבים מתארים 'תור' בדומה לתור בדרך לקופה בסופרמרקט, (בהנחה שאף אחד לא עוקף). הראשון בתור, הוא הראשון שיש להניח שיסיים את החשבון, השני בתור הוא השני שיסיים וכו'. אנו ניצור מופעים של thread   יצרן ששם נתונים בתור ו thread  צרכן שמקבל נתונים מתוך התור, byte  אחד בכל פעם בשני המקרים. אנו נביא כל אחד מה threads  למצב שינה-sleep(), לפרק זמן שרירותי בין הניסיונות לשים או לקחת נתונים.

 נכתוב מחלקת QueueManager שמנהלת את ההכנסה של נתונים לתוך התור ואת הוצאתם ממנו באופן שמיישם את מודל היצרן/ צרכן. ניתן לדמות את ה QueueManager לשוטר תנועה המכוון את התנועה בצומת עמוס ומנסה למנוע משתי מכוניות להתנגש באמצע הצומת.

המקבילה בתיכנות לשתי מכוניות הנפגשות באמצע הצומת מתבטאת לעיתים קרובות במצב שנוהגים לכנות "מצב מרוץ"- race condition . רוב ספרי ג'אווה כוללים תוכניות הדגמה שממחישות את מצב ה race condition. מומלץ לך לבחון אחת מהן. ה QueueManager ישתמש במתודות ה- wait() ו- notify() כדי למנוע התנגשויות.

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

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

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

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

עד לנקודה מסוימת, עם בעיה מסוג זה, הסבירות לפגוש תור מלא או ריק יורדת בהתאם לגודל הכללי של התור. להלן תוכנית ההדגמה שלנו. תוכנית זו ממחישה סנכרון של threads שהוא רעיון בעל חשיבות רבה.

 

 המנחה שלך יסביר את פעולת התוכנית מלבד הקוד של מחלקת התור (the Queue class) .

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

 

 

מוניטורים (Monitors)

ישנו רעיון עתיק יומין במדעי המחשב שמכונה מוניטור- monitor כפי שיתואר להלן. אובייקטים כמו התור שבדוגמא לעיל ש threads שונים חולקים אותם ושהגישה אליהם חייבת להיות מסונכרנת (לעבור תיאום בו-זמני) ידועים בשם 'משתני תנאי'.

לפי Campione & Walrath "שפת הג'אווה ומערכת זמן-הריצה תומכת בסנכרון של threads תוך שימוש במוניטורים monitors – שתוארו באופן כללי לראשונה במאמר של C.A.R Hoare :  Communicating Sequential Processes( קישור תהליכים רציפים)

 (שנמצא ב- Communication of the ACM, כרך 21, מס' 8, אוגוסט 1978, עמ' 666-677).

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

אם לשני  threads או יותר יש קוד שמגיע לאותם נתונים, הקוד הזה ידוע כ- critical section. בג'אווה אתה משתמש במילת המפתח synchronized כדי לסמן critical sections של קוד. בדרך כלל תסמן מתודות שלמות כ- critical sections  באמצעות מילת המפתח synchronized. כמו כן ניתן לסמן יחידות קוד קטנות יותר כ synchronized.

יחד עם זאת, לפי Campione & Walrath : "זה פוגע בתבניות מונחות-עצמים ומביא לקוד מבלבל שקשה לדבג ולשמר אותו. למרבית מטרות התיכנות שלך בג'אווה, הכי טוב להשתמש ב synchronized רק ברמת המתודה."

מוניטור מיוחד מיוחס לכל אובייקט שיש לו מתודה מסונכרנת. אם היינו עושים מופעים ליותר מאובייקט אחד של מחלקת ה- QueueManager לכל אובייקט של המחלקה הזו היה את המוניטור המיוחד שלו.

יש נקודה חשובה מאוד שמסבירים בספר “Java Primer Plus” מאת Tyma, Torok ו- Downing: "לכל אובייקט יש רק מנעול אחד. לכן, אם לאובייקט יש שלוש מתודות מסונכרנות, thread שנכנס לאחת מהמתודות האלה 'ינעל' את המנעול. אחר כך, שום thread לא יוכל להיכנס לאף אחת מהמתודות המסונכרנות עד שהראשון יוצא מהמתודה שהפעיל ומשחרר את המנעול.

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

לסיכום, כשפקד (control) נכנס  למתודה מסונכרנת, ה thread   שקרא למתודה רוכש את המנעול על האובייקט. אובייקטים אחרים לא יכולים להפעיל כל מתודה מסונכרנת אחרת על האובייקט הזה עד שהמנעול משוחרר.

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

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

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

כמו כן, בדוגמא שלנו הצרכן  מפעיל את wait() על תור ריק תוך שהוא משחרר את המנעול באופן זמני  ומאפשר גישה ליצרן (שיכניס בית לתור כדי שהוא לא יהיה ריק). במהלך פעולה ('לא מלאה'=  (non-full רגילה, כשחוזרת מתודת  putByteInQueue(), היצרן משחרר את הנעילה על התור. כשהצרכן קורא למתודת ה- getByteFromQueue הצרכן רוכש מנעול לתור שמונע מהיצרן לקרוא למתודת ה- putByteINQueue.

 

המוניטורים של ג'אווה יכולים להיות מוכנסים-מחדש (Re-entrant)

לפי Campione & Walrath  : " מערכת ה runtime  של ג'אווה מאפשרת ל thread לרכוש מחדש מוניטור בו הוא כבר מחזיק מכיוון שהמוניטורים של ג'אווה ניתנים להכנסה מחודשת (re-entrant). מוניטורים הניתנים להכנסה מחודשת חשובים מכיוון שהם מוציאים מכלל אפשרות מצב שבו  thread בודד ינעל את עצמו באופן בלתי הפיך על מוניטור שבו הוא כבר מחזיק.  הם גם מספקים תוכנית דוגמא  שממחישה את הערך של תכונה זו, אם ברצונך ללמוד על כך יותר.

 

המתודות notify() ו- wait()

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

ה QueueManager משתמש ב notify() ו- wait() כדי למנוע התנגשויות במהלך הניסיונות של הצרכן לקבל נתונים מהתור.  היצרן או הצרכן בלבד אך לא שניהם יחד רשאים לגשת אל התור בזמן נתון מסוים. זו תמציתו של המוניטור. רק thread  אחד יכול להיות במוניטור בכל זמן שהוא. המתודות notify() ו- wait() שייכות למחלקה java.lang.Object .

מתודות אלה ניתן להפעיל רק מתוך מתודה מסונכרנת או מתוך הצהרה או בלוק  (block)מסונכרנים.

 

המתודה notify()

המתודה getByteFromQueue קוראת ל notify() בסוף המתודה. המתודת notify() בוחרת thread שנמצא בהמתנה (waiting) ומעירה אותו. במקרה של תוכנית ההדגמה שלנו, הצרכן מחזיק את המנעול על התור במהלך הפעלתה של מתודת  ה getByteFromQueue.

בדיוק לפני סיומה  המתודה getByteFromQueue קוראת ל notify() להעיר את היצרן אם הוא במצב המתנה (waiting) . אם הוא לא ממתין, פשוט מתעלמים מהקריאה ל notify().

כהערה צדדית, אומרים Campione & Walrath: "אם מספר רב של  threads מחכה למוניטור, מערכת זמן-הריצה  של ג'אווה בוחרת אחד מה threads שמחכים , ללא כל מחויבות או עירבון לגבי בחירתם של מי מה threads. "

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

לדברי Campione & Walrath: "למחלקה Object יש מתודה נוספת - NotifyAll() - שמעירה את כל ה threads  שמחכים לאותו המוניטור. במצב זה, ה threads שהתעוררו מתחרים על המוניטור. אחד מהם מקבל את המוניטור והשאר חוזרים להמתנה."

 

המתודה wait()

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

 ·          מפעילה את המתודה wait()

 ·           משחררת את המנעול שלה מהתור  ו-

 ·           מחכה שהמתודה putByteInQueue() תפעיל את notify() כדי להעיר אותו.

שחרור המנעול מאפשר למתודה putByteInQueue() לשים בית חדש בתור ולגרום שהוא לא יהיה ריק יותר.

PutByteInQueue() אם כן, מפעילה את notify() בכדי להעיר את putByteInQueue(). כש thread נכנס למתודת wait() המוניטור משתחרר, וכשה thread יוצא מהמתודה wait() המוניטור נרכש שנית.

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

 

גרסאות אחרות של המתודה wait()

מחלקת Object מכילה שתי גרסאות נוספות למתודה wait() שמתעוררות אוטומטית (לא מחכות עד אין סוף להודעה):

 ·           wait(long timeout) - מחכה להודעה או עד שתקופת פסק-הזמן (timeout) חולפת, פסק הזמן נמדד באלפיות השנייה.  

 ·            wait(long timeout, int nanos)- מחכה להודעה או עד שהפסק זמן באלפיות השנייה ובננו- שניות (אחד חלקי מיליארד השנייה) חולף.

 

קיפאון (Deadlock)

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

Campione & Walrath נותנים הסבר טוב למצב קלאסי של קיפאון שנהוג לכנותו בשם

 the Dining Philosophers ("השולחן העגול של החכמים הסועדים") יחד עם כמה הצעות איך למנוע קיפאון. מומלץ לך ללמוד את החומר הזה. הגרסה המקוונת כוללת applet (- תוכנית קטנה) שמאפשרת לך להתנסות בפרמטרים המשפיעים על הפוטנציאל לקיפאון.

 12-10-03 / 22:23  עודכן ,  11-10-03 / 12:08  נוצר ע"י רונית רייכמן  בתאריך 
 עדיפויות לגבי threads - הקודםהבא - תוכנית סנכרון 
תגובות הקוראים    תגובות  -  0
דרכונט
מהי מערכת הדרכונט?
אינך מחובר, להתחברות:
דוא"ל
ססמא
נושאי לימוד
חיפוש  |  לא פועל
משלנו  |  לא פועל
גולשים מקוונים: 2