CodeGym /בלוג Java /Random-HE /ניהול שרשורים. מילת המפתח הפכפכה ושיטת yield()
John Squirrels
רָמָה
San Francisco

ניהול שרשורים. מילת המפתח הפכפכה ושיטת yield()

פורסם בקבוצה
היי! אנו ממשיכים במחקר שלנו על ריבוי הליכים. היום נכיר את volatileמילת המפתח ואת yield()השיטה. בואו נצלול פנימה :)

מילת המפתח ההפכפכה

בעת יצירת יישומים מרובים, אנו יכולים להיתקל בשתי בעיות חמורות. ראשית, כאשר פועלת אפליקציה מרובה הליכי, שרשורים שונים יכולים לאחסן את הערכים של משתנים (כבר דיברנו על כך בשיעור שכותרתו 'שימוש בהפכפך' ). אתה יכול לקבל את המצב שבו שרשור אחד משנה את הערך של משתנה, אבל שרשור שני לא רואה את השינוי, כי הוא עובד עם העותק השמור שלו של המשתנה. מטבע הדברים, ההשלכות יכולות להיות חמורות. נניח שזה לא סתם משתנה ישן אלא יתרת חשבון הבנק שלך, שפתאום מתחילה לקפוץ באקראי מעלה ומטה :) זה לא נשמע כיף, נכון? שנית, ב-Java, פעולות לקריאה וכתיבה של כל הסוגים הפרימיטיביים, למעט longו- double, הן אטומיות. ובכן, למשל, אם תשנה ערך של משתנה intבשרשור אחד, ובשרשור אחר תקרא את הערך של המשתנה, תקבל את הערך הישן שלו או את הערך החדש, כלומר הערך שנבע מהשינוי בשרשור 1. אין 'ערכי ביניים'. עם זאת, זה לא עובד עם longs ו- doubles. למה? בגלל תמיכה בין פלטפורמות. זוכרים ברמות ההתחלה שאמרנו שהעיקרון המנחה של Java הוא 'כתוב פעם אחת, רץ בכל מקום'? זה אומר תמיכה חוצת פלטפורמות. במילים אחרות, אפליקציית Java פועלת על כל מיני פלטפורמות שונות. לדוגמה, במערכות הפעלה Windows, גרסאות שונות של Linux או MacOS. זה יפעל ללא תקלות על כולם. שוקל 64 סיביות, longוהם doubleהפרימיטיבים 'הכבדים' ביותר בג'אווה. ופלטפורמות מסוימות של 32 סיביות פשוט אינן מיישמות קריאה וכתיבה אטומית של משתני 64 סיביות. משתנים כאלה נקראים וכותבים בשתי פעולות. ראשית, 32 הסיביות הראשונות נכתבות למשתנה, ולאחר מכן נכתבות עוד 32 סיביות. כתוצאה מכך עלולה להיווצר בעיה. שרשור אחד כותב איזה ערך של 64 סיביות Xלמשתנה ועושה זאת בשתי פעולות. במקביל, חוט שני מנסה לקרוא את הערך של המשתנה ועושה זאת בין שתי הפעולות הללו - כאשר 32 הסיביות הראשונות נכתבו, אך 32 הסיביות השניות לא. כתוצאה מכך, הוא קורא ערך ביניים, שגוי, ויש לנו באג. לדוגמה, אם בפלטפורמה כזו ננסה לכתוב את המספר ל -9223372036854775809 למשתנה, הוא יתפוס 64 סיביות. בצורה בינארית זה נראה כך: 100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 השרשור הראשון מתחיל לכתוב את המספר למשתנה. בהתחלה, הוא כותב את 32 הסיביות הראשונות (1000000000000000000000000000000000000000000000000000000000000) ולאחר מכן את 32 הביטים השניים (000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001) והפתיל השני יכול להיתקע בין הפעולות האלה, קריאת ערך הביניים של המשתנה (1000000000000000000000000000000000000000000000000000000000), שהם 32 הביטים הראשונים שכבר נכתבו. בשיטה העשרונית, מספר זה הוא 2,147,483,648. במילים אחרות, רק רצינו לכתוב את המספר 9223372036854775809 למשתנה, אבל בשל העובדה שהפעולה הזו אינה אטומית בפלטפורמות מסוימות, יש לנו את המספר המרושע 2,147,483,648, שהגיע משום מקום ויהיה לו השפעה לא ידועה תכנית. השרשור השני פשוט קרא את הערך של המשתנה לפני שסיים להיכתב, כלומר השרשור ראה את 32 הסיביות הראשונות, אבל לא את 32 הסיביות השניות. כמובן שהבעיות האלה לא התעוררו אתמול. Java פותרת אותם עם מילת מפתח אחת: volatile. אם נשתמש במילת volatileהמפתח כאשר אנו מצהירים על משתנה כלשהו בתוכנית שלנו...
public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…זה אומר ש:
  1. זה תמיד ייקרא ויכתוב בצורה אטומית. גם אם זה 64 סיביות doubleאו long.
  2. מכונת ה-Java לא תשמור אותה במטמון. אז לא יהיה לך מצב שבו 10 שרשורים עובדים עם עותקים מקומיים משלהם.
לפיכך, שתי בעיות חמורות מאוד נפתרות במילה אחת בלבד :)

שיטת yield()

כבר סקרנו הרבה מהשיטות Threadשל הכיתה, אבל יש אחת חשובה שתהיה חדשה עבורכם. זו yield()השיטה . והוא עושה בדיוק את מה שהשם שלו מרמז! ניהול שרשורים.  מילת המפתח הפכפכה ושיטת yield() - 2כשאנחנו קוראים לשיטה yieldעל שרשור, היא למעשה מדברת עם השרשורים האחרים: 'היי, חבר'ה. אני לא ממהר במיוחד ללכת לשום מקום, אז אם חשוב למישהו מכם לקבל זמן מעבד, קחו אותו - אני יכול לחכות'. הנה דוגמה פשוטה לאיך זה עובד:
public class ThreadExample extends Thread {

   public ThreadExample() {
       this.start();
   }

   public void run() {

       System.out.println(Thread.currentThread().getName() + " yields its place to others");
       Thread.yield();
       System.out.println(Thread.currentThread().getName() + " has finished executing.");
   }

   public static void main(String[] args) {
       new ThreadExample();
       new ThreadExample();
       new ThreadExample();
   }
}
אנו יוצרים ומתחילים שלושה שרשורים ברצף: Thread-0, Thread-1, ו Thread-2. Thread-0מתחיל קודם ומיד נכנע לאחרים. ואז Thread-1הוא התחיל וגם מניב. ואז Thread-2מתחיל, שגם מניב. אין לנו יותר שרשורים, ואחרי Thread-2שנתן את מקומו האחרון, מתזמן השרשור אומר, 'הממ, אין יותר שרשורים חדשים. מי יש לנו בתור? מי נתן את מקומו לפני כן Thread-2? נראה שכן Thread-1. אוקיי, זה אומר שאנחנו ניתן לזה לרוץ'. Thread-1משלים את עבודתו ואז מתזמן השרשור ממשיך בתיאום שלו: 'אוקיי, Thread-1סיימתי. יש לנו עוד מישהו בתור?'. שרשור-0 נמצא בתור: הוא הניב את מקומו ממש לפני Thread-1. עכשיו זה מגיע לתורו ורץ להשלמה. ואז המתזמן מסיים לתאם את השרשורים: 'אוקיי, Thread-2, נכנעת לשרשורים אחרים, וכולם סיימו עכשיו. אתה היית האחרון שנכנע, אז עכשיו תורך'. ואז Thread-2רץ לסיום. פלט הקונסולה ייראה כך: Thread-0 נותן את מקומו לאחרים Thread-1 נותן את מקומו לאחרים Thread-2 נותן את מקומו לאחרים Thread-1 סיים להפעיל. Thread-0 הסתיים להפעיל. Thread-2 הסתיים להפעיל. כמובן, מתזמן השרשורים עשוי להתחיל את השרשורים בסדר שונה (לדוגמה, 2-1-0 במקום 0-1-2), אך העיקרון נשאר זהה.

קורה-לפני חוקים

הדבר האחרון שניגע בו היום הוא המושג ' קורה לפני '. כפי שאתה כבר יודע, ב-Java מתזמן השרשורים מבצע את עיקר העבודה הכרוכה בהקצאת זמן ומשאבים לשרשורים לביצוע המשימות שלהם. ראית שוב ושוב כיצד שרשורים מבוצעים בסדר אקראי שבדרך כלל בלתי אפשרי לחזות אותו. ובאופן כללי, לאחר התכנות ה'רציף' שעשינו קודם, תכנות מרובה הליכי נראה כמו משהו אקראי. כבר הגעת להאמין שאתה יכול להשתמש במגוון שיטות כדי לשלוט בזרימה של תוכנית מרובה הליכי. אבל לריבוי השרשורים בג'אווה יש עוד נדבך אחד - 4 הכללים ' קורה-לפני '. הבנת הכללים הללו היא די פשוטה. תארו לעצמכם שיש לנו שני חוטים - Aו B. כל אחד מהשרשורים הללו יכול לבצע פעולות 1ו 2. בכל כלל, כאשר אנו אומרים ' A קורה-לפני B ', אנו מתכוונים לכך שכל השינויים שנעשו על ידי שרשור Aלפני הפעולה 1והשינויים הנובעים מפעולה זו גלויים לשרשור Bבעת 2ביצוע הפעולה ולאחריה. כל כלל מבטיח שכאשר אתה כותב תוכנית מרובה הליכי, אירועים מסוימים יתרחשו לפני אחרים ב-100% מהזמן, וכי בזמן ההפעלה 2השרשור Bתמיד יהיה מודע לשינויים שהשרשור Aעשה במהלך הפעולה 1. בואו נסקור אותם.

חוק מספר 1.

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

כלל 2.

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

כלל 3.

סוף השיטה run()קורה לפני החזרה מהשיטה join(). נחזור לשני השרשורים שלנו: Aו B. אנו קוראים join()לשיטה כך שפתיל Bמובטח ימתין להשלמת השרשור Aלפני שהוא יעשה את עבודתו. המשמעות היא ששיטת האובייקט A run()מובטחת לפעול עד הסוף. וכל השינויים בנתונים שקורים בשיטת run()השרשור Aמובטחים במאה אחוז שיהיו גלויים בשרשור Bברגע שהוא יסתיים בהמתנה שהשרשור Aיסיים את עבודתו כדי שיוכל להתחיל בעבודה משלו.

כלל 4.

volatileכתיבה למשתנה מתרחשת לפני קריאה מאותו משתנה. כאשר אנו משתמשים במילת volatileהמפתח, אנו למעשה תמיד מקבלים את הערך הנוכחי. אפילו עם longאו double(דיברנו קודם על בעיות שיכולות לקרות כאן). כפי שכבר הבנתם, שינויים שנעשו בשרשורים מסוימים לא תמיד גלויים לשרשורים אחרים. אבל, כמובן, יש מצבים תכופים מאוד שבהם התנהגות כזו לא מתאימה לנו. נניח שאנו מקצים ערך למשתנה ב-thread A:
int z;.

z = 555;
אם Bהשרשור שלנו צריך להציג את הערך של zהמשתנה במסוף, הוא יכול בקלות להציג 0, כי הוא לא יודע על הערך שהוקצה. אבל כלל 4 מבטיח שאם נכריז על zהמשתנה כ- volatile, אז שינויים בערכו בשרשור אחד תמיד יהיו גלויים בשרשור אחר. אם נוסיף למילה volatileלקוד הקודם...
volatile int z;.

z = 555;
...אז אנו מונעים את המצב שבו שרשור Bעשוי להציג 0. כתיבה volatileלמשתנים מתרחשת לפני הקריאה מהם.
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION