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 הוא אובייקט א-סינכרוני אמיתי, ולא בטוח שבשורה הבאה ההודעה ששלחנו כבר טופלה והערך אכן נשמר (כמובן שאם יש צורך להבטיח יש שיטות, אבל אז המודל כבר לא א-סינכרוני). ההבנה הזו גורמת לשינוי של כל הדרך בה הקוד בנוי – מסדרת הוראות שמתבצעות אחת אחרי השניה, לאובייקטים שמטפלים באירועים ושולחים הודעות זה לזה. זה הסגנון שהופך את המקביליות לשקופה, ומעודד בידוד של הרכיבים במערכת בצורה שמקנה לה אמינות.
הפוסט הבא יהיה האחרון בסדרה ונראה בו עוד שתי דוגמאות לשימוש במקביליות.

כתיבת תגובה

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

הלוגו של WordPress.com

אתה מגיב באמצעות חשבון WordPress.com שלך. לצאת מהמערכת / לשנות )

תמונת Twitter

אתה מגיב באמצעות חשבון Twitter שלך. לצאת מהמערכת / לשנות )

תמונת Facebook

אתה מגיב באמצעות חשבון Facebook שלך. לצאת מהמערכת / לשנות )

תמונת גוגל פלוס

אתה מגיב באמצעות חשבון Google+ שלך. לצאת מהמערכת / לשנות )

מתחבר ל-%s