Events Thread Safety Hazards
דבר אחד שימושי בדוט-נט הוא התמיכה הטבעית הקיימת
ל-Event'ים. כדי ליצור אירוע למחלקה שלנו, כל מה שעלינו לעשות הוא להגדיר
Event חדש בעזרת ה-Delegate המתאים, וזהו זה. שימו לב שאפילו לא היינו
צריכים לכתוב Accessors כלשהם, מאחר וכברירת מחדל, אלא אם כן לא הגדרנו
אחרת, הקומפיילר יחולל כבר Accessor'ים משלו שידאגו לנהל עבורנו את
הרישומים עבור האירוע שהגדרנו. זוהי נקודה שהרבה פעמים לוקחים כמובן
מאליו, ולא מפנים אליה תשומת לב מיוחדת, ולמרות זאת, מסתתרת מאחוריה
משמעות קריטית כש-Multithreading נכנס לתמונה.
הרעיון הוא כזה, מה קורה
במידה וננסה להוסיף/להסיר הרשמויות לאותו האירוע מתוך מספר ת'רדים שונים?
קיים כאן עניין של סנכרון שאנו מוכרחים לטפל בו.
אז כדי לבדוק באמת מה קורה, נכתוב דוגמה קצרה ונראה איזה קוד IL נקבל.
אז קוד הדוגמה שלנו יהיה:
public class Foo
{
public event EventHandler Bar;
}
הדבר הראשון שנוכל לראות ב-IL הוא שחוללו לנו ה-Accessors הנחוצים עבור האירוע. ניקח את Add לצורך ההדגמה:
והנה, בחתימת הפונקציה אנחנו יכולים להבחין במילת הקסם synchronized שנעשה בה שימוש על מנת להעניק לנו את אותו Thread Safety נחשק.
אבל בעצם, מה מילת הקסם הזאת אומרת? ומה
כל כך רע בה?
אז ככה, synchornized היא הדרך של הקומפיילר להדריך את ה-JIT שאת הקטע קוד
הבא יש לבצע רק על ידי ת'רד בודד בכל רגע נתון. האלמנט הזה מתווסף
אוטומטית כשאנחנו כותבים Event'ים ללא מימוש מפורש של Accessors, ומעבר
לכך, אנחנו יכולים לאלץ הוספה שלו בקוד על ידי שימוש ב Attribute של
MethodImpl (עם הדגל Synchronized).
הסיבה ששימוש ב-synchronized הוא כל כך רע הוא צורת הנעילה שלו. כשה-JIT
נתקל בפונקציה שמוגדרת עם synchronized הוא עוטף את כל קטע הקוד שלה עם
Monitor.Enter כשאובייקט הנעילה הוא המופע שמכיל את ה-Event, או הטיפוס
שלו (כשמדובר ב-Event'ים סטאטים). כלומר, הקוד הנ"ל שווה ערך לקוד
הבא כשמדובר ב-Instance Event:
public class Foo
{
private EventHandler m_bar;
public event EventHandler Bar
{
add
{
Monitor.Enter(this);
m_bar = m_bar + value;
Monitor.Exit(this);
}
remove
{
Monitor.Enter(this);
m_bar = m_bar - value;
Monitor.Exit(this);
}
}
}
ולקוד הבא כשמדובר ב-Static Event:
public static class Foo
{
private static EventHandler s_bar;
public static event EventHandler Bar
{
add
{
Monitor.Enter(typeof(Foo));
s_bar = s_bar + value;
Monitor.Exit(typeof(Foo));
}
remove
{
Monitor.Enter(typeof(Foo));
s_bar = s_bar - value;
Monitor.Exit(typeof(Foo));
}
}
}
רק כדי לעשות את זה מעט ברור יותר, אין לנעול לעולם על this או typeof.
וליתר דיוק, על כל אובייקט שהוא Publicly Accessed. הכוונה היא שבעוד
שאנחנו נועלים אותו, יכולים להגיע גם אנשים אחרים, מחוץ לקוד שלנו (למשל
מודול כלשהו שמשתמש בטיפוס שלנו), ולנעול אותו גם כן. כל הנעילות האלה
שמתבצעות ללא הכרה אחת של השניה יכולות לגרום בקלות ל-Deadlock'ים
"נסתרים" בקוד. המצב עוד יותר גרוע כשמנסים לנעול את typeof מאחר וה-Type
נגיש לכל ה-AppDomain שאנו נמצאים בו.
מכאן, ששימוש ב-Events ללא מימוש מפורש של Add/Remove, הם פתח לאי אלו
התנהגויות "מעניינות" בתוכנית שלכם ברגע שמערבים Multithreading. ויש
לדאוג להמנע מכך, או לפחות לנהוג בזהירות יתרה.
מה שאירוני כאן זה שבכל מקום שלא תחפשו, תתקלו באינספור
הזהרות והוראות ממיקרוסופט עצמה לא לנעול בצורה הזאת. והנה, בלי ששמנו לב אותה נעילה מסוכנת נעשית ממש מתחת לאף שלנו, בלי שבכלל שמנו לב.
עדכון 22.03.2010
החל מגרסא 4 של #C, חילול הקוד של ה-Event Accessors השתנה כך שכבר לא יעשה שימוש ב-syncblock של אובייקט ה-this. לעומת זאת, השימוש ב-Monitor הוסר וכעת נעזרים ב-CAS על מנת לשמור על ה-Thread Safety. כל זה מפורט בסדרת הפוסטים הבאה של Chris Burrow:
חלק א',
חלק ב',
חלק ג'.