201. Event leaks

הכרנו בעבר (טיפ מספר 193) את המושג של Eventים.

אחת הבעיות הנפוצות עם Eventים היא הבאה:

Eventים יכולים לגרום לדליפת זיכרון אם לא משתמשים בהם נכון.

איך זה קורה?

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

מה שיכול לקרות זה שאנחנו נרשמים לאובייקט שחי יותר זמן מאיתנו. מה שזה גורר זה שהאובייקט שחי יותר זמן, מחזיק Delegate שמצביע לאובייקט שלנו. כתוצאה מכך הוא מחזיק גם Reference לאובייקט שלנו. מה שזה גורר זה שהGarbage Collector לא אוסף אותנו, כי הוא רואה שלמישהו (האובייקט שחי יותר זמן מאיתנו) יש Reference אלינו.

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

להלן דוגמה הממחישה את הסיפור:

יש את המחלקות הבאות:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Subject
{
public event EventHandler EventRaised;
}
public class Observer
{
public Observer(Subject source)
{
source.EventRaised += new EventHandler(OnEventRaised);
}
private void OnEventRaised(object sender, EventArgs e)
{
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Subject subject = new Subject();
long memoryBefore = GC.GetTotalMemory(true);
List<Observer> observers = new List<Observer>(50000);
for (int i = 0; i < 50000; i++)
{
observers.Add(new Observer(subject));
}
observers = null;
GC.Collect();
long memoryAfter = GC.GetTotalMemory(true);
Console.WriteLine(memoryAfter - memoryBefore); // 2662244

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

בדוגמה הזאת מדובר באובייקטים שאינם כבדים ולא שוקלים הרבה, כך שזה פחות מורגש (שימו לב שיצרתי 50000 אובייקטים בשביל לגדול בפחות מ3 מגה)

בחיים האמיתיים מה שקורה זה שהאובייקטים הרבה יותר כבדים, הם יכולים להיות Formים למשל שנרשמים לEventים אחרים וככה הדליפה יכולה להיות מורגשת…

הדרך הקלאסית לפתור את בעיה זו היא ליצור פונקציה שמבטלת את הרישום לEventים אלה:

נוכל, למשל, לממש IDisposable (ראו טיפ מספר 62):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Subject subject = new Subject();
long memoryBefore = GC.GetTotalMemory(true);
List<Observer> observers = new List<Observer>(50000);
for (int i = 0; i < 50000; i++)
{
observers.Add(new Observer(subject));
}
foreach (Observer observer in observers)
{
observer.Dispose();
}
observers = null;
GC.Collect();
long memoryAfter = GC.GetTotalMemory(true);
Console.WriteLine(memoryAfter - memoryBefore); // 800068

כפי שאתם רואים, הפעם השתחרר לנו יותר זכרון.

הפתרון הזה סבבה והכל, אבל יש איתו רק בעיה אחת – הוא דוחה.

למה דוחה?

הרגילו אותנו לחשוב שהשפה היא שפה Managed, ושהGarbage Collector יודע לנהל את הזכרון עבורנו.

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

בהמשך אנחנו נראה פתרון אחר לבעיה זו,

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

שתף