JavaRush /בלוג Java /Random-HE /ניהול זרימה. מילת המפתח הפכפכה ושיטת yield()

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

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

מילת מפתח נדיפה

בעת יצירת יישומים מרובים, אנו יכולים להתמודד עם שתי בעיות חמורות. ראשית, במהלך ההפעלה של אפליקציה מרובה הליכי שרשורים שונים יכולים לאחסן את ערכי המשתנים במטמון (על כך נדבר יותר בהרצאה "שימוש בנדיף" ). יתכן שרשור אחד שינה את הערך של משתנה, אבל השני לא ראה את השינוי הזה מכיוון שהוא עבד עם עותק מטמון משלו של המשתנה. מטבע הדברים, ההשלכות יכולות להיות חמורות. תארו לעצמכם שזה לא סתם איזה "משתנה", אלא למשל היתרה של כרטיס הבנק שלכם, שפתאום התחילה לקפוץ באקראי הלוך ושוב :) לא נעים במיוחד, נכון? שנית, בג'אווה, פעולות קריאה וכתיבה על שדות מכל הסוגים למעט longוהם doubleאטומיים. מהי אטומיות? ובכן, למשל, אם תשנה את הערך של משתנה בשרשור אחד int, ובשרשור אחר תקרא את הערך של המשתנה הזה, תקבל או את הערך הישן שלו או את הערך החדש - זה שהתברר לאחר השינוי ב- שרשור 1. לא יופיעו שם "אפשרויות ביניים" אולי. עם זאת, זה לא עובד עם longו . doubleלמה? כי זה חוצה פלטפורמות. אתה זוכר איך אמרנו ברמות הראשונות שעיקרון הג'אווה "נכתב פעם אחת, עובד בכל מקום"? זה חוצה פלטפורמות. כלומר, אפליקציית Java פועלת על פלטפורמות שונות לחלוטין. לדוגמה, במערכות הפעלה של Windows, גרסאות שונות של לינוקס או MacOS, ובכל מקום יישום זה יעבוד ביציבות. longוכן double- הפרימיטיבים ה"כבדים" ביותר בג'אווה: הם שוקלים 64 סיביות. וכמה פלטפורמות של 32 סיביות פשוט לא מיישמות את האטומיות של קריאה וכתיבה של משתנים של 64 סיביות. משתנים כאלה נקראים וכותבים בשתי פעולות. ראשית, 32 הסיביות הראשונות נכתבות למשתנה, ואז עוד 32. בהתאם לכך, במקרים אלו עלולה להיווצר בעיה. שרשור אחד כותב איזה ערך של 64 סיביות למשתנהХ, והוא עושה את זה "בשני שלבים". במקביל, השרשור השני מנסה לקרוא את הערך של המשתנה הזה, ועושה זאת בדיוק באמצע, כאשר 32 הסיביות הראשונות כבר נכתבו, אך השניים עדיין לא נכתבו. כתוצאה מכך, הוא קורא ערך ביניים, שגוי, ומתרחשת שגיאה. לדוגמה, אם בפלטפורמה כזו ננסה לכתוב מספר למשתנה - 9223372036854775809 - הוא יתפוס 64 סיביות. בצורה בינארית זה יראה כך: 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 הביט תחילה, ותחילה יכתוב את ה-00000 סיביות ראשונות למשתנה, ויכתוב תחילה את ה-00000 סיביות ראשונות: 00000 000000 00000 ואז השני 32: 0000000000000000000000000000000001 וחוט שני יכול להתקע לתוך הפער הזה קרא את ערך הביניים של המשתנה - 1000000000000000000000000000000000, 32 הסיביות הראשונות שכבר נכתבו. במערכת העשרונית, המספר הזה שווה ל-2147483648. כלומר, רק רצינו לכתוב את המספר 9223372036854775809 למשתנה, אבל בשל העובדה שהפעולה הזו בפלטפורמות מסוימות אינה אטומית, קיבלנו את המספר "שמאלי" 2147483648 , שאנחנו לא צריכים, משום מקום.ולא ידוע איך זה ישפיע על פעולת התוכנית. השרשור השני פשוט קרא את הערך של המשתנה לפני שהוא נכתב לבסוף, כלומר, הוא ראה את 32 הסיביות הראשונות, אבל לא את 32 הסיביות השניות. הבעיות הללו, כמובן, לא התעוררו אתמול, וב-Java הן נפתרות באמצעות מילת מפתח אחת בלבד - 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כשאנחנו קוראים לשיטת התשואה על שרשור, זה בעצם אומר לשרשורים אחרים: "אוקיי, חבר"ה, אני לא ממהר במיוחד, אז אם זה חשוב למישהו מכם לקבל זמן מעבד, קחו את זה, אני לא דחוף." הנה דוגמה פשוטה לאיך זה עובד:
public class ThreadExample extends Thread {

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

   public void run() {

       System.out.println(Thread.currentThread().getName() + "give way 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 הושלם. יש לנו עוד מישהו בתור?" יש Thread-0 בתור: הוא ויתר על מקומו מיד לפני Thread-1. כעת הגיע אליו העניין, והוא מבוצע עד הסוף. לאחר מכן מסיים המתזמן לתאם את השרשורים: "בסדר, שרשור-2, פינית את מקומו לשרשורים אחרים, כולם כבר עבדו. אתה היית האחרון שנכנע, אז עכשיו תורך". לאחר מכן, Thread-2 פועל עד לסיומו. פלט הקונסולה ייראה כך: Thread-0 מפנה את מקומו לאחרים Thread-1 מפנה את מקומו לאחרים Thread-2 מפנה את מקומו לאחרים Thread-1 סיים להפעיל. Thread-0 הסתיים להפעיל. Thread-2 הסתיים להפעיל. מתזמן השרשורים, כמובן, יכול להריץ שרשורים בסדר אחר (לדוגמה, 2-1-0 במקום 0-1-2), אבל העיקרון זהה.

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

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

חוק מספר 1.

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

כלל 2.

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

כלל 3.

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

כלל 4.

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

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

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