195. Events and framework 4.0

פעם שעברה הכרנו את הAccessorים של Event.

בין השאר ראינו את הקוד שהקומפיילר כותב עבור Eventים להם אין Accessorים מפורשים וראינו שהמימוש הוא משהו כזה:

הקוד הזה

1
2
3
4
public class EventRaiser
{
public event EventHandler Raised;
}

מתמקפל למשהו ששקול לזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class EventRaiser
{
private EventHandler mRaised;
public event EventHandler Raised
{
add
{
mRaised += value;
}
remove
{
mRaised -= value;
}
}
}

זה נכון, עד לC# 4.0. מי שיפתח Reflector על קוד שקומפל לFramework 4.0 יראה את הקוד הבא:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class EventRaiser
{
private EventHandler mRaised;
public event EventHandler Raised
{
add
{
EventHandler handler2;
EventHandler raised = mRaised;
do
{
handler2 = raised;
EventHandler handler3 = (EventHandler)Delegate.Combine(handler2, value);
raised = Interlocked.CompareExchange<EventHandler>(ref mRaised, handler3, handler2);
}
while (raised != handler2);
}
remove
{
EventHandler handler2;
EventHandler raised = this.mRaised;
do
{
handler2 = raised;
EventHandler handler3 = (EventHandler)Delegate.Remove(handler2, value);
raised = Interlocked.CompareExchange<EventHandler>(ref this.mRaised, handler3, handler2);
}
while (raised != handler2);
}
}
}

כשראיתי את הקוד הזה לראשונה לא כל כך הבנתי מה קורה כאן.

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

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

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

1
2
3
4
5
6
raised = mRaised;
if (mRaised == handler2)
{
mRaised = handler3;
}

כלומר מחליף את mRaised בDelegate המשורשר בתנאי שmRaised עדיין שווה לhandler2 ומכניס לraised את הערך המקורי שלmRaised. ההבדל המשמעותי בין הקוד שרשמתי פה לשימוש בInterlocked.CompareExchange, זה שInterlocked.CompareExchange הוא אטומי. כלומר, 3 השורות שציינתי מתבצעות בפעולת מעבד אחת, ולכן זה Thread Safe.

כעת הדבר חוזר על עצמו כל עוד raised לא שווה לhandler2.

אנחנו נצא מהלולאה כאשר raised יהיה שווה לhandler2, כלומר כאשר mRaised לפני השרשור של הEvent שווה לmRaised אחרי שרשור הEvent. כלומר כאשר לא התערב לנו Thread אחר בערך של mRaised.

זהו, בסה"כ זה די מפחיד, אבל אפשר להבין מה קורה כאן.

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

סוף שבוע טוב עם הרבה אירועים בטוחים!

שתף