סיסמאות ואנטרופיה – האורך כן קובע

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

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

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

בואו ניתן דוגמא נוספת – תחנת מזג אויר שולחת עבור כל יום שתי מדידות: האם היה קר או לא, והאם היה גשום או לא. באיזור שנמדד 75% מהימים הם קרים וגשומים, עוד 15% קרים ויבשים, רק 7% גשומים וחמים והיתר יבשים וחמים. הנתונים נשלחים במקבצים של 100 ימים כל פעם.

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

00: חם ויבש

01: חם וגשום

10: קר ויבש

11: קר וגשום

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

עכשיו נחשוב על הפרוטוקול הבא:

0 – קר וגשום

10 – קר ויבש

110 – יום חם וגשום

1110 – יום חם ויבש

גם הפרוטוקול הזה פשוט לעבודה שכן 0 משמש כחוצץ (כל הודעה של יום מסוים מסתיימת ב0),  אבל על פניו הוא פחות יעיל מהראשון שכן אורך ההודעה הממוצע הוא 2.5 ביט לעומת 2 בראשון.  האמת היא שבנתוני השאלה הפרוטוקול הזה יעיל יותר שכן לכל מאה יום נשלח בערך 75 הודעות של תו אחד, 15 של שני תוים וכו', ובסך הכל מקבץ של 100 ימים ידרוש 138 ביט בלבד!

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

אותיות ומספרים – האמנם?

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

האנטרופיה של סיסמא מסוימת זהה לכמות הניסיונות שיש לעשות כדי לפצח אותה. סיסמא בת 6 תוים מהאלף בית האנגלי תכיל 266 פרמוטציות, ובאופן כללי הצורה הי­­­א   nm כאשר n הוא מספר האפשרויות לכל תו וm הוא אורך המחרוזת.

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

באופן יותר קונקרטי, ניקח כדוגמא סיסמא בת 6 אותיות. עכשיו נכריח את המשתמשים להשתמש בכל מה שיש למקלדת להציע – אותיות קטנות וגדולות, מספרים וסמלים מיוחדים, סה"כ 72 תוים. האנטרופיה החדשה היא 726 או 1.39314E+11.

עכשיו נבדוק כיוון אחר – נשתמש רק באותיות, אפילו ללא רגישות לאותיות קטנות וגדולות, ונוסיף סה"כ 2 תוים לסיסמא. עכשיו נקבל 228שהם 1.20727E+12, כלומר הרוחנו יותר על ידי פעולה פשוטה של הוספת 2 תוים לאורך, מאשר על ידי פעולה מסורבלת של הגדלת מס' התוים האפשריים.

מבחינת שימושיות ברור שלאדם יותר קל לזכור סיסמאות ארוכות ובעלות משמעות (למשל הסיסמא my password is strong) מאשר סיסמאות מוזרות עם ספרות ותוים כמו pa$$w0rD ולכן לא ברור מי חשב שעדיף לקבל מעט אנטרופיה ובקושי (על ידי הגדלת האפשרויות לכל תו) בזמן שאפשר לקבל הרבה אנטרופיה, ובקלות (על ידי סיסמאות ארוכות שניתן לכתוב באנגלית פשוטה).

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

בעזרת הwaze

בשבוע שעבר נסענו לאירוע משפחתי, ובדרך פתחנו את waze כדי להחליט האם לצאת מירושלים ביציאה הראשית או דרך מנהרת הארזים.בשלב מסוים התחלנו לקבל הוראות שלא רק סותרות את מה שחשבנו אלא נראות לא הגיוניות בכלל. האינסטינקט במקרים כאלה הוא להניח שיש באג בתוכנה ולסמוך על הדרך שעשית כבר כל כך הרבה פעמים, אבל אנחנו למודי ניסיון, שמנו את דעתנו בצד וצייתנו להוראות. בשלב מסוים קלטתי שהמסלול שלנו נכנס לגבעת שאול וחשבתי שהמטרה היא לצאת בגינות סחרוב, אבל בסוף התברר שהמסלול אפילו יותר אקזוטי מזה – נסענו דרך יער ירושלים והתחברנו לכביש 1 ביציאה של מוצא – נקודת החיבור האחרונה האפשרית לפקק הקטסטרופלי שהיה שם. באירוע האנשים התחלקו בצורה ברורה: משתמשי WAZE הגיעו תוך 35 דקות והשאר תוך 55 דקות! (ותראו מה קרה לגוטמן בדרך לחדר לידה).

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

  • כתבתי כבר בעבר על האתגרים והדילמות של בינה מלאכותית ומהם התפקידים שבהם המחשב יכול להחליף את האדם. האמת היא שבwaze היפוך התפקידים כבר הושלם: בעוד שהסמארטפון עסוק בפעילות אינטלקטואלית, מחשב מסלולים ומשמיע הוראות, לייצור המגושם בן האנוש שלצידו נותר רק לציית, ולעסוק בפעילות המכנית הגסה של לחיצה על דוושות, סיבוב ההגה וכיוצ"ב.
  • Waze מכונה לפעמים גם "רשת חברתית לנהגים", אבל בניגוד לרשת חברתית שבה המסרים הם יחידות מידע נפרדות שנוצרות על ידי המשתמשים (או באופן אוטומטי כמו בetoro למשל) ומתארות את המציאות, המסרים שעוברים בwaze הם המציאות עצמה והם מושפעים מהמציאות וגם משפיעים עליה. כאשר אני בנתיב שהתנועה בו איטית, התוצאה תהיה שמתשתמש אחר יופנה לנתיב פנוי יותר וזה מתרחש בזכות תקשורת שקיימת בינינו לא רק בלי שאף אחד מאיתנו יטרח על העברת מסרים או פיענוחם בצורה מודעת, אלא שאנו אפילו לא בהכרח מודעים האחד לקיומו של האחר!
  • בעקבות כך מעניין אותי לדעת מתי נתחיל לראות hacking על הרשת של waze. רוב האנשים מקשרים את המונח האקר לתחום המחשבים, אבל האמת היא שהאקר הוא כל מי שיכול לבצע מניפולציה על מערכת כלשהיא מתוך היכרות יסודית עם הדרך בה היא פועלת. במובן הזה עורך דין שמחלץ עבריין מעונש או רואה חשבון שבונה מקלטי מס מתוך הכרה מדוקדקת של החוק הם האקרים לכל דבר ועניין. מניפולציות על waze לא דורשות פריצה של התוכנה או התקשורת, אלא יכולות להיעשות על ידי שימוש רגיל בתכונות שהתוכנה מציעה: נניח שאני מארגן מספר לא גדול של חבר'ה שמדליקים waze ואז, תוך נסיעה בכביש החוף, מאיטים ביחד את מהירות הנסיעה ובסוף זורקים את הטלפונים שלהם לתוך רכב שממתין בשוליים. יכול מאוד להיות שאני גורם לזיהוי מוטעה של פקק כבד בכביש החוף וכתוצאה מכך להפניה של נהגים לאיילון! את ההשלכות אפשר לדמיין לבד, החל ממעשי קונדס וכלה בכל מיני רעיונות שאני מקווה שנראה רק בסרטים.

העם דורש צדק תחבורתי

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

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

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

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

בירה להמונים

הערה

ביום שני הקרוב (26.11) יערך מפגש של קבוצת groovy and grails israel.
המפגש הפעם יוקדש להיכרות עם הטכנולוגיות, ולכן מתאים גם (ואולי בעיקר) לחסרי ניסיון ומתעניינים בתחום.

מי שעוקב אחרי הבלוג אמור להכיר את groovy – שפה דינאמית עבור הJVM ואת Grails – פלטפורמת פיתוח שמצד אחד דומה לRuby on rails בתכונותיה המתקדמות ומצד שני ממנפת את התשתית הרצינית שקיימת בפלטפורמת JAVA.

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

המפגש מתארח בחסות חברת JFrog. פרטים נוספים בלינק.
בואו בהמוניכם..

take a memo

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

 //swap a & b

temp = a

a=b

b=temp

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

//xor swap a & b

a=a+b

b=a-b

a=a-b

הצלחנו לחסוך את משתנה העזר בעזרת 3 פעולות חישוב.

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

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

def greet = {arg ->

    println 'running'

    return "hello ${arg}"

}

println greet("Joe")

println greet("Matt")

println greet("Joe")

def mem = greet.memoize()

println mem("Joe")

println mem("Matt")

println mem("Joe")

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

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

Avoid side effects

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

Class flight{

int availableSeats=100

def book = {seats ->

                def confirmed = false;

                if (availablePlaces-places  >=  0){

                                confirmed=true

                                availeablePlaces -= places

                }

Return confirmed

}

המטודה book משנה ערך שמוגדר מחוץ לטווח שלה ולכן ברור שהיא איננה מתאימה לאופטימיזציה כזו (מה יקרה אם יקראו לה 3 פעמים עם הארגומנט 50?).

Memoizing javascript

בשפה גמישה ודינאמית כמו JS ניתן 'להלביש' יכולות של memoization אף על פי שזה לא מבנה שהוא חלק מהשפה. הדוגמא הבאה ממחישה זאת (למרות שזה לא המימוש הכי יעיל):

Function.prototype.memoize = function(){

    var self = this, cache = {};

    return function( arg ){

      if(arg in cache) {

        console.log('Cache hit for '+arg);

        return cache[arg];

      } else {

        console.log('Cache miss for '+arg);

        return cache[arg] = self( arg );

      }

    }

  }

function fib( x ) {

    if(x < 2) return 1; else return fib(x-1) + fib(x-2);

}

var fibTest = memoize(fib);

console.log(fibTest(40)); //run slow

console.log(fibTest(40)); //cached, returns immediately

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

איך להחזיר חוב (טכנולוגי)

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

איך חוב נולד

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

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

התכונה השנייה של חוב מוכרת לכל מי שקיבל דרישת תשלום על חוב קטן מלפני 20 שנה, וזוכה להכיר בעובדה שהחוב לא ישן בשקט אלא תופח עם הזמן. גם חוב טכנולוגי נושא ריבית שמחריפה אותו עם הזמן. בואו נניח בדוגמא הקודמת שכל פיגור של 10 יחידות תחזוקה דורש יחידת תחזוקה אחת כדי לכסות אותו, דבר שנובע מעודף עבודה שצריך לבצע כדי לגשר על הפערים של המערכת (למשל חסר מודול דוחות ומנהל בסיס הנתונים מפיק אותם באופן ידני). מאחר וכל שנה אנו משקיעים 5 יחידות תחזוקה במקום 10, לאחר 10 שנים ניצור פיגור של 50 יחידות שהתחזוקה שלו בלבד עולה 5 יחידות בשנה, כלומר מאותו רגע כל ההשקעה בתחזוקה הולכת על תשלום ה'ריבית' של החוב, והמערכת לקראת קריסה טוטאלית שכן בפועל לא נותרו שום משאבים להשקיע בשוטף. (האמת היא שזה יקח פחות מ10 שנים בגלל אפקט הריבית דריבית – הרי כבר בשנה הראשונה יצרנו פיגור של 5 יחידות שיעלה חצי יחידה בשנה השניה וישאיר רק 4.5 יחידות להשקעה אמיתית וכך בכל שנה, לכן החוב יגיע לגודל הזה הרבה יותר מהר.)

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

החזר חובות 101

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

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

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

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

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

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

GPars – Agent & Data Flow

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

Agent

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

 

import groovyx.gpars.agent.Agent

agent = new Agent([])

agent.send {it.add 1}

agent.send {it.add 2}

sleep(1000)

println agent.val

 

הקוד הזה ממחיש כמה מהעקרונות של Agent:

בשורה הראשונה יוצרים Agent שמכיל רשימה ריקה. למעשה Agent יכול לעטוף אוביקטים מכל סוג ולהוסיף להם יכולות.

בשורה השניה והשלישית מוסיפים ערכים לרשימה על ידי שליחה של קוד לביצוע (Closure). הAgent אחראי לבצע את הקוד על ידי Thread משלו.

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

בשורה החמישית רואים את ההשפעה של מה שהוספנו.

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

 

Public void setX(int x){

                this.x=x;

}

 

בטוח שזה קוד מיותר – למה לקרוא למטודה במקום פשוט לקבוע את הערך של x מבחוץ? אבל בעצם זה קוד חשוב מאוד. הוא מעביר את השליטה במידע לאוביקט שמנהל אותו. באופן דומה Agent עושה הכמסה לדרך בה הקוד מתבצע – שולחים אליו בקשות אבל הוא זה ששולט בסדר ובזמן הביצוע שלהן.

נקודה נוספת שחשוב לשים לב אליה היא הגמישות הרבה שמקבל הAPI ברגע שניתן לשלוח closure כארגומנט. נראה עוד דוגמא:

 

class Point{

                int x,y

}

agent2 = new Agent(new Point())

agent2.send {it.x=5;it.y=7}

sleep 1000

println agent2.val.y

 

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

Data flow

הדוגמא האחרונה שנראה בקצרה היא של מודל חישובי בשם Data Flow. המודל נשען על ההנחות הבאות:

  1. ניתן לחלק עיבוד גדול למספר תתי עיבודים.
  2. הקלטים של תת עיבוד מסוים הם הפלטים של תתי עיבודים אחרים.
  3. במהלך העיבוד כל ערך מחושב פעם אחת בלבד (במילים אחרות, כל תת עיבוד ירוץ פעם אחת בלבד).

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

 

Calculate{

If (! a.hasValue) a.wait()

If (! b.hasValue) b.wait()

Return a+b

}

setA(value){

                a=value

                a.notify()

}

setB(value){

                b=value

                b.notify()

}

 

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

העבודה עם DataFlow דומה. הנה דוגמא בסיסית ביותר:

 

import static groovyx.gpars.dataflow.Dataflow.task

import groovyx.gpars.dataflow.DataflowVariable

final def x = new DataflowVariable()

final def y = new DataflowVariable()

final def z = new DataflowVariable()

task {

    z << x.val + y.val

}

task {

    x << 10

}

task {

    y << 5

}

println "Result: ${z.val}"

 

כמובן שהסינטקס פשוט וכל פעולות ההמתנה והאיתות נעשות מתחת למכסה המנוע. כמו כן קיים הבדל חשוב נוסף – כמו במודלים הקודמים אין מיפוי של תתי חישובים לThreads של מערכת ההפעלה, אלא התשתית משתמשת בThread pool קיים ומתזמנת את ההקצאות שלו למשימות שצריכות לרוץ. זה מאפשר יצירה של המון 'תהליכונים' מאחר והם קלים מאוד יחסית לנימים של מערכת הפעלה. בדף של GPars ניתן לראות עוד דוגמאות רבות ליכולות של Data Flow ולשימושים מתקדמים שניתן לממש.

סיכום

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

GPars דף התיעוד הראשי של הסיפרייה. רוב דוגמאות הקוד נילקחו מכתובת זו או מבוססות עליה.

AKKA ספרייה עשירה בשפת JAVA (למעשה scala) לשימוש בActors

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

GPars – Actors

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

על קצה המזלג

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

Every man is an island

אחד ההסברים מדוע מודל של Actors הוא מוצלח לתכנות מקבילי הוא שכמו כל מודל טוב הוא משקף את המציאות. בעולם שלנו אין זיכרון משותף לכל האנשים שכל אחד יכול לשנות אלא כל אחד מסתובב עם המחשבות והזיכרונות של עצמו וכאשר אנחנו נדרשים לתקשר עם אדם אחר אנחנו מעבירים לו הודעה בצורה זו או אחרת של תקשורת. בתפיסה הזו Actors הוא מודל 'טבעי' שקל לנו לחשוב בו, בניגוד למודל של זיכרון משותף וסנכרון שמסבך אותנו וגורם לטעויות. בואו נראה את היתרונות של המימוש בדוגמא הקודמת שהבאנו:
• אין זיכרון (state) משותף. כל שחקן הוא אי בודד של מידע שהוא בלבד אחראי לטפל בו, ותיאום בין שחקנים שונים מתבצע על ידי העברת הודעות.
• המודל יצר הפשטה ברמה גבוהה של נושא המקביליות. הטיפול במקביליות ובסנכרון הוא גנרי וזהה לכל אפליקציה (מתרכז במשלוח וצריכה של הודעות) ואילו קוד האפליקציה חופשי משיקולי מקביליות ומכיל רק לוגיקה עסקית.
• בגלל שכל שחקן נדרש לפעול רק כאשר יש לו הודעות ממתינות בתור, אין צורך לקשור כל שחקן לThread אלא אפשר להשתמש במנגנון תזמון שמקצה לשחקנים את הThreads הנחוצים להם. כתוצאה מכך שחקן הוא אוביקט הרבה יותר קומפקטי מThread וניתן לייצר כמות גדולה של מופעים.

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

import groovyx.gpars.actor.DynamicDispatchActor

import groovyx.gpars.actor.DefaultActor

import groovyx.gpars.actor.Actors

final class MyDDA extends DynamicDispatchActor {

def myState=0;

void onMessage(String message) {

myState+=1

println "Received string ${myState}"

}

void onMessage(Integer message) {

myState+=2

println "Received integer ${myState}"

}

}

final def myActor = new MyDDA().start()

myActor 'Hello'

myActor 12

 

בדוגמא יצרנו Actor שמטפל בהודעות לפי pattern matching כלומר תופעל המטודה שמתאימה לסוג ההודעה שהגיעה. הפקודה start() גרמה לו להתחיל להאזין ולטפל בהודעות, ואז שולחים אליו שתי הודעות (זה סינטקס אחד מכמה צורות אפשריות). שימו לב למשתנה myState – הוא בפועל Thread safe אפילו שאין לנו שום טיפול בעניין הזה בקוד. הדבר הזה נובע מכך שברגע נתון תטופל הודעה אחת לכל היותר.

תכונה יפה היא היכולת להוסיף תמיכה בסוגי הודעות בזמן ריצה:

final def myActor = new MyDDA().become {

when {BigDecimal num -> myState+=5; println "Received BigDecimal ${myState}"}

when {Float num -> myState+=6; println "Got a float ${myState}"}

}.start()

בדוגמא למעלה מוסיפים תמיכה בשני סוגים חדשים של הודעות תוך כדי ריצה. הדבר הזה מאפשר עדכון דינאמי של התנהגות הActor מבלי לעצור אותו.
להמחשה האחרונה נסתכל על הדוגמא הבאה שנמצאת בתוך גוף של Actor בשם mapWrapper:
 

def myMap=[:]

void onMessage(List message) {

def key = message[0]

def value = message[1]

if (myMap.get(key) == null){

myMap.put(key, value)

}

}

מה שיש לנו כאן זה מבנה נתונים אסוציאטיבי (map) שמתנהג בפועל בסמנטיקה של Thread safety. אם נשלח במקביל את אותו מפתח עם ערכים שונים, לעולם לא תתבצע דריסה (הערך הראשון יתפוס). זו סמנטיקה זהה לפעולה put if absent של concurrent map והשגנו אותה ללא כל שימוש בקוד סנכרון, מצד שני יש הבדל משמעותי:

concurrentMap.putIfAbsent(key, value)

//key value is set

mapWrapper [key, value]

//key value may or may not be set

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

GPars – מערכים מקביליים

הדוגמאות הראשונות שנראה בנושא של GPars הם די פשוטות וישירות ועוסקות בטיפול מקבילי באלמנטים של רשימה.

נתחיל בדוגמא הכי פשוטה – מעבר על הרשימה והדפסת כל האיברים

import groovyx.gpars.ParallelEnhancer

def list = 1..10000

//sequential

list.each { println it + ',' + Thread.currentThread()}

//concurrent

ParallelEnhancer.enhanceInstance(list)

list.asConcurrent {

                start = System.currentTimeMillis()

    list.each {println it + ',' + Thread.currentThread()}

}

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

דוגמא נוספת

import static groovyx.gpars.GParsPool.withPool

long start = System.currentTimeMillis()

def list1 = [1000,1000,1000,1000,1000,1000,1000,1000,1000,1000]

list1.each{ sleep it}

long con = System.currentTimeMillis()

withPool(10){

                list1.eachParallel{ sleep it.toLong()}

}

println "sequential: ${con-start}, concurrent: ${System.currentTimeMillis()-con}"

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

למה זה טוב?

יש שני תסריטים שבהם היכולת הזו היא מאוד שימושית.

הראשון הוא כאשר הקוד חסום בגלל משהו שאינו המעבד – למשל גישה לרשת. הרצת כמה פעולות כאלה במקביל חוסכת 'הצטברות' של הזמן בו ממתינים לתגובה משרתים אחרים ומאפשרת שיפור ביצועים אפילו על מעבד בודד. זה נקרא Fork/Join ונכנס גם לשפת ג'אווה עצמה. בגרובי כרגיל התחביר קצת יותר פשוט וידידותי. הדוגמא הבאה לקוחה מהתיעוד של GPars ומראה סריקה של מילים בכמה כתובות URL במקביל. אנשי ג'אווה מוזמנים לדמיין איך יראה אצלם קוד שעושה את זה:

def urls = ['http://www.dzone.com', 'http://www.theserverside.com', 'http://www.infoq.com']

withPool{

println 'Number of occurrences of the word GROOVY today: ' + urls.parallel

                .map {it.toURL().text.toUpperCase()}

                .filter {it.contains('GROOVY')}

                 .map{it.split()}

                 .map{it.findAll{word -> word.contains 'GROOVY'}.size()}

                .sum()

}

Map/reduce מכה שנית

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

def words = "This is just a plain text to count words in"

print count(words)

def count(arg) {

  withPool {

    return arg.parallel

 .map{[it, 1]}

.combine(0) {sum, value -> sum + value}.getParallel()

                .sort{-it.value}.collection

                }

}

(השורה המעניינת היא combine והיא מגדירה איך לבצע אגרגציה בין מפתחות שונים)

מטודות א-סינכרוניות

עוד יכולת נחמדה היא לקחת מטודות קיימות ולהפוך אותן בקלות לא-סינכרוניות. לאחר מכן ניתן לקרוא להן כרגיל אבל הן ירוצו ברקע. כמובן שניתן גם לחסום את המשך הריצה כדי להמתין שמטודה שרצה ברקע תסתיים (כמו Future). הנה דוגמא קטנה שעושה sleep sort:

// groovy sleep sort

def sleeper = {number -> sleep number*100; print "${number}, "}

withPool(10){

                def sorter = sleeper.asyncFun()

                [10,6,3,8,5,7,2,4,9].each{sorter it}

}

בפרק הבא…

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