CyclicBarrier בג'אווה

1. הקדמה

מחסומים מחזוריים הם מבני סנכרון שהוצגו עם Java 5 כחלק מה- java.util.concurrent חֲבִילָה.

במאמר זה נחקור יישום זה בתרחיש מקבילי.

2. מקביליות Java - סינכרוניזרים

ה java.util.concurrent החבילה מכילה מספר שיעורים המסייעים בניהול מערך שרשורים המשתפים פעולה זה עם זה. חלקם כוללים:

  • CyclicBarrier
  • פאזר
  • CountDownLatch
  • חַלְפָן
  • סֵמָפוֹר
  • SynchronousQueue

שיעורים אלה מציעים פונקציונליות מחוץ לקופסה לדפוסי אינטראקציה נפוצים בין שרשור.

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

בואו נתמקד ב CyclicBarrier הולך קדימה.

3. CyclicBarrier

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

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

המכשול נקרא מַחזוֹרִי מכיוון שניתן להשתמש בו מחדש לאחר שחרור החוטים הממתינים.

4. שימוש

הקונסטרוקטור עבור א CyclicBarrier זה פשוט. נדרש מספר שלם יחיד המציין את מספר האשכולות שצריכים לקרוא ל לְהַמתִין() שיטה במופע המכשול כדי לסמן להגיע לנקודת הביצוע המשותפת:

ציבור CyclicBarrier (צדדים אינטנסיביים)

החוטים שצריכים לסנכרן את הביצוע שלהם נקראים גם מסיבות ומתקשר ל לְהַמתִין() השיטה היא כיצד אנו יכולים לרשום שחוט מסוים הגיע לנקודת המחסום.

שיחה זו סינכרונית והשרשור שקורא לשיטה זו משעה את הביצוע עד שמספר שרשורים שצוין קרא לאותה שיטה במחסום. מצב זה בו מספר הטלפונים הנדרש התקשר לְהַמתִין(), נקרא מעד את המחסום.

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

CyclicBarrier ציבורי (צדדים אינטנסיביים, Runnable barrierAction)

5. יישום

לראות CyclicBarrier בפעולה, נבחן את התרחיש הבא:

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

בואו ניישם את המעמד הראשי שבו כל הפעולות מתרחשות:

מחלקה ציבורית CyclicBarrierDemo {private CyclicBarrier cyclicBarrier; רשימה פרטית partialResults = Collections.synchronizedList (ArrayList חדש ()); פרטי אקראי אקראי = אקראי חדש (); פרטי int NUM_PARTIAL_RESULTS; NUM_WORKERS פרטיים; // ...}

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

לבסוף, יש לנו חלקי תוצאות שזו רשימה שתאחסן את התוצאות של כל אחד מהשרשורים העובדים האלה. שים לב שרשימה זו היא רשימת סינכרונים מכיוון שרבים מהשרשורים יכתבו אליו בו זמנית, וה- לְהוֹסִיף() השיטה אינה בטוחה בחוטים במישור רשימת מערך.

עכשיו בואו לממש את ההיגיון של כל אחד מחוטי העובד:

מחלקה ציבורית CyclicBarrierDemo {// ... מחלקה NumberCruncherThread מיישמת ניתנת להפעלה {@Override public void run () {String thisThreadName = Thread.currentThread (). getName (); רשימת partialResult = ArrayList חדש (); // קראץ כמה מספרים ושמור את התוצאה החלקית עבור (int i = 0; i <NUM_PARTIAL_RESULTS; i ++) {Integer num = random.nextInt (10); System.out.println (thisThreadName + ": מחץ מספרים! תוצאה סופית -" + מספר); partialResult.add (num); } partialResults.add (partialResult); נסה את {System.out.println (thisThreadName + "מחכה שאחרים יגיעו למחסום."); cyclicBarrier.await (); } לתפוס (InterruptedException e) {// ...} לתפוס (BrokenBarrierException e) {// ...}}}}

כעת נבצע את ההיגיון שמתנהל לאחר שהמכשול הופעל.

כדי לפשט את העניין פשוט בואו נוסיף את כל המספרים ברשימת התוצאות החלקית:

מחלקה ציבורית CyclicBarrierDemo {// ... מחלקה AggregatorThread מיישמת Runnable {@Override public void run () {String thisThreadName = Thread.currentThread (). getName (); System.out.println (thisThreadName + ": סכום המחשוב של" + NUM_WORKERS + "עובדים, עם תוצאות" + NUM_PARTIAL_RESULTS + "כל אחד."); סכום int = 0; עבור (רשימת threadResult: partialResults) {System.out.print ("מוסיף"); עבור (Integer partialResult: threadResult) {System.out.print (partialResult + ""); סכום + = partialResult; } System.out.println (); } System.out.println (thisThreadName + ": תוצאה סופית =" + סכום); }}}

השלב האחרון יהיה לבנות את CyclicBarrier ולבעוט דברים עם א רָאשִׁי() שיטה:

מחלקה ציבורית CyclicBarrierDemo {// קוד קוד public void runSimulation (int numWorkers, int numberOfPartialResults) {NUM_PARTIAL_RESULTS = numberOfPartialResults; NUM_WORKERS = מספר עובדים; cyclicBarrier = CyclicBarrier חדש (NUM_WORKERS, AggregatorThread חדש); System.out.println ("שרצים" + NUM_WORKERS + "שרשורי עובדים לחישוב" + NUM_PARTIAL_RESULTS + "תוצאות חלקיות כל אחד"); עבור (int i = 0; i <NUM_WORKERS; i ++) {Thread עובד = שרשור חדש (NumberCruncherThread חדש ()); worker.setName ("חוט" + i); worker.start (); }} ראשי ריק סטטי ציבורי (String [] args) {CyclicBarrierDemo demo = CyclicBarrierDemo new (); demo.runSimulation (5, 3); }} 

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

לאחר שהמכשול מועד, החוט האחרון שהכשיל את המחסום מבצע את ההיגיון שצוין ב- AggregatorThread, כלומר - הוסף את כל המספרים שמייצרים החוטים.

6. תוצאות

הנה הפלט מביצוע אחד של התוכנית הנ"ל - כל ביצוע עשוי ליצור תוצאות שונות מכיוון שניתן להשריץ את האשכולות בסדר אחר:

משריץ 5 אשכולות עובדים כדי לחשב 3 תוצאות חלקיות כל אחד אשכול 0: מחץ כמה מספרים! תוצאה סופית - 6 נושא 0: מחץ כמה מספרים! תוצאה סופית - 2 נושא 0: מחץ מספרים! תוצאה סופית - 2 חוטים 0 מחכים לאחרים להגיע למחסום. שרשור 1: מחץ כמה מספרים! תוצאה סופית - 2 נושא 1: מחץ מספרים! תוצאה סופית - 0 נושא 1: מחץ מספרים! תוצאה סופית - חוט 1 מחכה לאחרים להגיע למחסום. שרשור 3: מחץ כמה מספרים! תוצאה סופית - 6 נושא 3: מחץ מספרים! תוצאה סופית - 4 נושא 3: מחץ מספרים! תוצאה סופית - 0 שרשור 3 ממתין לאחרים להגיע למחסום. שרשור 2: מחץ כמה מספרים! תוצאה סופית - 1 נושא 2: מחץ מספרים! תוצאה סופית - 1 נושא 2: מחץ מספרים! תוצאה סופית - 0 שרשור 2 ממתין לאחרים להגיע למחסום. שרשור 4: מחץ כמה מספרים! תוצאה סופית - 9 אשכול 4: מחץ כמה מספרים! תוצאה סופית - 3 נושא 4: מחץ כמה מספרים! תוצאה סופית - 5 אשכול 4 מחכה לאחרים להגיע למחסום. שרשור 4: מחשוב סופי של 5 עובדים, עם שלוש תוצאות כל אחד. הוספת 6 2 2 הוספת 2 0 5 הוספת 6 4 0 הוספת 1 1 0 הוספת 9 3 5 חוט 4: תוצאה סופית = 46 

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

7. מסקנה

במאמר זה ראינו מה א CyclicBarrier הוא, ובאיזה סוג מצבים זה מועיל.

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

כמו תמיד, את הקוד ללימוד ניתן למצוא ב- GitHub.