تقييم الموضوع :
  • 0 أصوات - بمعدل 0
  • 1
  • 2
  • 3
  • 4
  • 5
Multicast Delegates & Memory Leaks
#1
مقال تقني: Memory Leaks In .NET
بيئة التطوير : جميع إصدارات الفيجوال ستوديو بدءًا من 2003
اللغة #C نفس ما سيرد في هذا الموضوع ينطبق على VB .
المستوى: متقدم (يفترض بقارء الموضوع أن يكون ملماً باساسيات الدوت نت ).

في اللغات الغير مدارة مثل C++ عليك أن تتذكر دائماً بأنه يجب عليك أن تتخلص من الذاكرة التي قمت بتخصيصها من أجل كائن معين عندما تصبح بدون حاجة إليه, وإلا فسيحدث ما يسمى بال Memory Leak أو تسرب الذاكرة , في عالم الدوت نت والمصادر المدارة, إمكانية حدوث ال Memory Leak شبه منعدمة في أغلب الأحيان , والفضل يعود إلى ال CLR وآلية إدارة الذاكرة التي يعتمدها من خلال ال Garbage Collection , ما يعني أن اي كائن يتم تخصيص مساحة من الذاكرة له يتم نحريرها فور انتهاء مدة حياة ذلك الكائن وحالما يصبح بدون أي References .لكن ما يحدث في التطبيقات التي تعمل باستمرار وحيث أن حجم الذاكرة المستهلكة يزداد كلما زادت فترة حياة ذلك التطبيق , فإنه من الممكن حدوث ال Memory Leaks . تحدث مشاكل الذاكرة هذه عادة بسبب الكائنات التي تبقى عالقة في الذاكرة من دون استخدامها داخل تطبيقك , مثال شائع عن مسببات ال Memory Leaks وهي ال MulticastDelegates وبالضبط ال EventHandlers فمثلا عندما تقوم بربط EventHndler من كائن A إلى EventHandler لكائن B ولم تقم بفصل هذا الارتباط فسيبقى الكائن B عالقاً في الذاكرة ولن تقوم GC بجمعه حتى عندما تسند القيمة null أو Nothing له لأنه لازال هناك مرجع من الكائن A يشير إلى الكائن B , والمثال التالي يوضح ذلك:
كود :
[align=left][FONT=Consolas][FONT=Consolas][color=blue]public[/color] [color=blue]class[/color] [color=#2b91af]Server[/color][/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]{[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas][color=blue]public[/color] [color=#2b91af]EventHandler[/color] handler;[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]}[/FONT][/FONT][/align]

[align=left][FONT=Consolas][FONT=Consolas][color=blue]public[/color] [color=blue]class[/color] [color=#2b91af]Client[/color]   [/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]{[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas][color=#2b91af]Server[/color] _server = [color=blue]null[/color];[/FONT][/FONT][/align]
[FONT=Consolas]

[align=left][FONT=Consolas][color=blue]public[/color] Client([color=#2b91af]Server[/color] server)[/FONT]
[FONT=Consolas]{[/FONT]
[FONT=Consolas]    [color=blue]this[/color]._server = server;[/FONT]
[FONT=Consolas]    [color=blue]this[/color]._server.handler += [color=blue]new[/color] [color=#2b91af]EventHandler[/color](Handle);[/FONT]
[FONT=Consolas]}[/FONT][/align]

[align=left][FONT=Consolas][color=blue]private[/color] [color=blue]void[/color] Handle([color=blue]object[/color] sender, [color=#2b91af]EventArgs[/color] e)[/FONT]
[FONT=Consolas]{[/FONT]
[FONT=Consolas]    [color=green]// Do something[/color][/FONT]
[FONT=Consolas]}[/FONT]
[FONT=Consolas]}[/FONT][/align]

[/FONT]


لنفترض بعد ذلك أنك قمت بإنشاء 2000 نسخة Instance من الفئة Client :
كود :
[align=left][FONT=Consolas][FONT=Consolas][color=blue]class[/color] [color=#2b91af]Test[/color][/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]{[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas][color=#2b91af]Server[/color] server = [color=blue]new[/color] [color=#2b91af]Server[/color]();[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas][color=blue]public[/color] Test()[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]{[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]    [color=blue]for[/color] ([color=blue]int[/color] i = 0; i < 2000; i++)[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]    {[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]        [color=blue]var[/color] client = [color=blue]new[/color] [color=#2b91af]Client[/color]([color=blue]this[/color].server);[/FONT][/FONT][FONT=Consolas]
[FONT=Consolas]    }[/FONT][/FONT][/align]
[FONT=Consolas]

[align=left][FONT=Consolas]}[/FONT]
[FONT=Consolas]}[/FONT][/align]


[align=left][FONT=Consolas]}[/FONT][/align]
[/FONT]
قد يخطر في بالك أنه بعد الانتهاء من استخدام ال 2000 كائن سيتم تدميرهم وتحرير الذاكرة التي كانو يشغلونها وهذا خاطئ , سيبقى الألفين كائن معلقين في الذاكرة والسبب ان كل منهم لا يزال يرتبط بالكائن server الخاص بالفئة Test عن طريق ال EventHandler حيث قمنا بإنشاء هذا الارتباط داخل الConstructor الخاص بالفئة Client ولكنا لم نقم بالتخلص من هذا الارتباط وذلك بعمل Unsubscribing لل Eventhandlers, وفي العادة يتم فصل هذا الارتباط عن طريق تحقيق الواجهة IDisposable للفئة Client ويكون ذلك داخل الإجراء Dispose كما هو موضح في الكود التالي:
كود :
[align=left][FONT=Consolas][FONT=Consolas][color=blue]public[/color] [color=blue]class[/color] [color=#2b91af]Client[/color] : [color=#2b91af]IDisposable[/color] [/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]{[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas][color=#2b91af]Server[/color] _server = [color=blue]null[/color];[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas][color=blue]bool[/color] disposed = [color=blue]false[/color];[/FONT][/FONT][/align]

[align=left][FONT=Consolas][FONT=Consolas][color=blue]public[/color] Client([color=#2b91af]Server[/color] server)[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]{[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]    [color=blue]this[/color]._server = server;[/FONT][/FONT][FONT=Consolas]
[FONT=Consolas]    [color=blue]this[/color]._server.handler += [color=blue]new[/color] [color=#2b91af]EventHandler[/color](Handle);[/FONT]
[FONT=Consolas]}[/FONT][/FONT][/align]
[FONT=Consolas]

[align=left][FONT=Consolas][color=blue]private[/color] [color=blue]void[/color] Handle([color=blue]object[/color] sender, [color=#2b91af]EventArgs[/color] e)[/FONT]
[FONT=Consolas]{[/FONT]
[FONT=Consolas]    [color=blue]if[/color] (disposed) [color=blue]throw[/color] [color=blue]new[/color] [color=#2b91af]ObjectDisposedException[/color]([color=#a31515]"client"[/color]);[/FONT]
[FONT=Consolas]    [color=green]// Do something[/color][/FONT]
[FONT=Consolas]}[/FONT][/align]

[align=left][FONT=Consolas][color=blue]public[/color] [color=blue]void[/color] Dispose()[/FONT]
[FONT=Consolas]{[/FONT]
[FONT=Consolas]    [color=blue]this[/color]._server.handler -= [color=blue]new[/color] [color=#2b91af]EventHandler[/color](Handle);[/FONT]
[FONT=Consolas]    [color=blue]this[/color].disposed = [color=blue]true[/color];[/FONT]
[FONT=Consolas]}[/FONT]
[FONT=Consolas]}[/FONT][/align]

[/FONT]


الآن وباستخدام using Statement أو استدعاء الإجراء Dispose يمكنك التأكد من أنه سيتم فصل الارتباط وتحرير ال 2000 كائن في المرة التالية التي يتم فيها عمل Garbage Collecting للذاكرة بعد الانتهاء من ال using Block كما يوضح المثال التالي:
كود :
[align=left][FONT=Consolas][FONT=Consolas][color=blue]class[/color] [color=#2b91af]Test[/color][/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]{[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas][color=#2b91af]Server[/color] server = [color=blue]new[/color] [color=#2b91af]Server[/color]();[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas][color=blue]public[/color] Test()[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]{[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]    [color=blue]for[/color] ([color=blue]int[/color] i = 0; i < 2000; i++)[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]    {[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]        [color=blue]using[/color] ([color=blue]var[/color] client = [color=blue]new[/color] [color=#2b91af]Client[/color]([color=blue]this[/color].server))[/FONT][/FONT][FONT=Consolas]
[FONT=Consolas]        {[/FONT]
[FONT=Consolas]            [color=green]// Do something...[/color][/FONT]
[FONT=Consolas]        }[/FONT]
[FONT=Consolas]    }[/FONT][/FONT][/align]
[FONT=Consolas]

[align=left][FONT=Consolas]}[/FONT]
[FONT=Consolas]}[/FONT][/align]
[/FONT]

وبالرغم من ذلك فإن هذا الحل لن يكون الأفضل فقد تضطر لعدم استخدام الواجهة Idisposable عندها لابد من استخدام الفئة WeakReference . سنتطرق إلى هذا الحل لاحقاً إن شاء الله.
الرد }}}}
تم الشكر بواسطة:
#2
المؤقتات Timers

يمكن أيضًا للمؤقتات المتوفرة قي الدوت نت أن تتسبب في تسرب الذاكرة , وهناك حالتان مختلفتان حسب نوع المؤقت الذي نتعامل معه , لنلق نظرة على الفئة Timer الموجودة ضمن مجال الأسماء System.Timers , بالنظر إلى المثال التالي فإنه عند إنشاء نسخة منها سيتم استدعاء الإجراء Timer_Tick كل ثانية:

كود :
[align=left][FONT=Consolas][FONT=Consolas][color=blue]class[/color] [color=#2b91af]TimerTest[/color][/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]{[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]  [color=#2b91af]Timer[/color] timer = [color=blue]new[/color] [color=#2b91af]Timer[/color] { Interval = 1000};[/FONT][/FONT][/align]

[align=left][FONT=Consolas][FONT=Consolas]  [color=blue]private[/color] TimerTest()[/FONT][/FONT][FONT=Consolas]
[FONT=Consolas]  {[/FONT]
[FONT=Consolas]      [color=blue]this[/color].timer.Elapsed += Timer_Tick;[/FONT]
[FONT=Consolas]      [color=blue]this[/color].timer.Start();[/FONT][/FONT][/align]
[FONT=Consolas]

[align=left][FONT=Consolas]  }[/FONT][/align]

[align=left][FONT=Consolas]  [color=blue]private[/color] [color=blue]void[/color] Timer_Tick([color=blue]object[/color] sender, [color=#2b91af]ElapsedEventArgs[/color] e)[/FONT]
[FONT=Consolas]  {[/FONT]
[FONT=Consolas]      [color=#2b91af]Console[/color].WriteLine([color=#a31515]"Tick!"[/color]);[/FONT]
[FONT=Consolas]  }[/FONT]
[FONT=Consolas]}[/FONT][/align]
[/FONT]
ولسوء الحظ , فإن هذا الكائن سيبقى عالق حتى ينتهي التطبيق ولن تتمكن GC من التقاطه والسبب أن الدوت نت نفسه يحتفظ بمرجع إلى جميع المؤقتات النشطة من النوع System.Timers.Timer من جهة , ومن جهة أخرى فإن كائن Timer نفسه مرتبط مع EventHandler الخاص بفئتك , وفي هذه الحالة لن يفيدك القيام ب Unsubscribing لل EventHandler وحده, بل عليك أيضًا استخدام الواجهة IDisposable واستدعاء الإجراء Dispose الخاص بالفئة Timer داخل الإجراء Dispose الخاص بفئتك. كما هو موضح في الكود التالي:
كود :
[align=left][FONT=Consolas][FONT=Consolas][color=blue]class[/color] [color=#2b91af]TimerTest[/color] : [color=#2b91af]IDisposable[/color] [/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]{[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]  [color=#2b91af]Timer[/color] timer = [color=blue]new[/color] [color=#2b91af]Timer[/color] { Interval = 1000};[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]  [color=blue]bool[/color] disposed = [color=blue]false[/color];[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]  [color=blue]private[/color] TimerTest()[/FONT][/FONT][FONT=Consolas]
[FONT=Consolas]  {[/FONT]
[FONT=Consolas]      [color=blue]this[/color].timer.Elapsed += Timer_Tick;[/FONT]
[FONT=Consolas]      [color=blue]this[/color].timer.Start();[/FONT][/FONT][/align]
[FONT=Consolas]

[align=left][FONT=Consolas]  }[/FONT][/align]

[align=left][FONT=Consolas]  [color=blue]private[/color] [color=blue]void[/color] Timer_Tick([color=blue]object[/color] sender, [color=#2b91af]ElapsedEventArgs[/color] e)[/FONT]
[FONT=Consolas]  {[/FONT]
[FONT=Consolas]      [color=blue]if[/color] (disposed) [color=blue]throw[/color] [color=blue]new[/color] [color=#2b91af]ObjectDisposedException[/color](GetType().FullName);[/FONT]
[FONT=Consolas]      [color=#2b91af]Console[/color].WriteLine([color=#a31515]"Tick!"[/color]);[/FONT]
[FONT=Consolas]  }[/FONT][/align]

[align=left][FONT=Consolas]  [color=blue]public[/color] [color=blue]void[/color] Dispose()[/FONT]
[FONT=Consolas]  {[/FONT]
[FONT=Consolas]      timer.Dispose();[/FONT]
      timer.Elapsed -= Timer_Tick;
[FONT=Consolas]      [color=blue]this[/color].disposed = [color=blue]true[/color];[/FONT]
[FONT=Consolas]  }[/FONT]
[FONT=Consolas]}[/FONT][/align]
[/FONT]

نفس الكلام السابق ينطبق على الفئة System.Windows.Forms.Timer .
الأمر مختلف بالنسبة للفئة System.Threading.Timer , فهو من نوع خاص, فالدوت نت لا تحتفظ بأي مراجع نحو المؤقتات النشطة من هذا النوع , ولكنها تحتفظ بال Callback Delegate الذي يتم تمريره لل Constructor وهذا يعني أنك إذا نسيت استدعاء الإجراء Dispose الخاص بالفئة Timer فسيتم ستدعاء ال Finalizer الخاص بها في أي لحظة ما يعني احتمال حدوث خطأ غير متوقع في سير عمل برنامجك بعد تدمير ال Timer , جرب المثال التالي في الوضع Release بدل الوضع Debug.
كود :
[align=left][FONT=Consolas][FONT=Consolas]      [color=#2b91af]Timer[/color] t = [color=blue]new[/color] [color=#2b91af]Timer[/color]([color=blue]delegate[/color] { [color=#2b91af]Console[/color].WriteLine([color=#a31515]"Tick"[/color]); }, [color=blue]null[/color], 1000, 1000);[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]      [color=#2b91af]GC[/color].Collect();[/FONT][/FONT]
[FONT=Consolas][FONT=Consolas]      [color=#2b91af]Thread[/color].Sleep(10000);[/FONT][/FONT][/align]
لاحظ أنه إذا قمت بتجربة الكود السابق فإن المؤقت لن يعمل ولو لمرة واحدة وسيتم عمل Finalizing له وجمعه عند استدعاء GC.Collect بشكل صريح. وحتى تتمكن من حل المشكلة لا بد من استخدام ال using Statement حتى تضمن أن GC لن يستطيع الوصول إلى المؤقت وجمعه. والاستدعاء الضمني للإجراء Dispose في نهاية ال using Block يضمن ذلك, ولكن استدعاء Dispose في هذه الحالة قد يطيل عمر المؤقت في الذاكرة!

معالجة مشاكل ال Memory Leaks:

أسهل طريقة يمكن من خلالها تجنب مشاكل تسرب الذاكرة من خلال قياس كمية الذاكرة المستهلكة طوال فترة كتابة البرنامج, يمكنك الحصول على قدر الذاكرة التي تستهلكها كائنات برنامجك باستخدام الإجراء GC.GetTotalMemory , حيث يمكن تمرير قيمة منطقية , فإن قمت بتمرير True فذلك سيجعل الGC تقوم بعمل Garbage Collecting أولاً:
كود :
[align=left][FONT=Consolas][color=#2b91af][COLOR=#2b91af][FONT=Consolas]Console[/FONT][/color][FONT=Consolas].WriteLine([color=#2b91af]GC[/color].GetTotalMemory([color=blue]true[/color]));[/FONT][/COLOR][/FONT][/align]
إذا كنت تعمل على تطبيق تستخدم فيه Unit Tests يمكنك القيام بعمليات التحقق من أن الذاكرة يتم تحريرها كما هو متوقع وإذا فشل أي تحقق Assertion فعليك التحقق عندها فقط من التغييرات الأخيرة التي قمت بها في تطبيقك.

إذا كنت تعمل على تطبيق ضخم وتعاني من مشكلة بسبب حدوث تسرب في الذاكرة , فقد تساعد الأداة windbg.exe على اكتشافها , هناك أيضأ بعض الأدوات التي تعتمد على واجهة رسومية تساعد على فهم مايحدث بالضبط مثل Microsoft CLR Profiler , و SciTech Memory Profiler , و RedGate ANTS Memory Profiler , وال CLR نفسه يحتوي على مجموعة من الأداوت المساعدة مثل WMI Counters التي تساعد على مراقبة الموارد Resources التي يستهلكها البرنامج.
الرد }}}}
تم الشكر بواسطة:


التنقل السريع :


يقوم بقرائة الموضوع: بالاضافة الى ( 1 ) ضيف كريم