تُخزَّن الكائنات المُنشئة باستخدام العَامِل new
بقسم الكَوْمَة المُدار في الذاكرة (managed heap). يُوفِّر اطار عمل .Net كَانِس المُهملات (Garbage Collector)، والذي يُدير الذاكرة ويُنْهِي (finalize) الكائنات المهملة دون أيّ تَدَخُّل صريح من المُبرمج.
تَستعرِض بعض الأمثلة التالية طريقة عمل كَانِس المُهملات وسُلوكه بشئ من التفصيل، ويُوضِح بعضها الآخر كيفية إعداد الأنواع بطريقة تَسمَح لكَانِس المُهملات بإدارتها بشكل سليم.
مفهوم الكائنات الحية والكائنات الميتة
كقاعدة عامة، يُعدّ الكائن حيًّا (live object) إذا ما زال بالإِمكان إعادة اِستخدَامه، ويُعدّ مَيتًا (dead object) إذا أصبح ذلك غير ممكن. ما يُحدِّد إِمكانية الاستخدَام مِن عدمه هو وجود مُتغيّر أو حَقْل واحد على الأقل يَحمِل مَرجِعًا (reference) إلى مكان الكائن بالذاكرة. في حالة خروج كل مُتغيّرات مَراجِع كائن ما -هذا إن وُجدت أساسًا في مرحلة ما أثناء التَّنفيذ- من النطاق (scope)، يُعدّ الكائن مَيتًا ويُنْهَى (finalize) عند إجراء عملية كَنْس.
مثال 1
في الشيفرة التالية، نُعرِّف النوع FinalizableObject
، والذي يَحوِي فقط باني النوع (constructor) ومُنْهِيه (finalizer):
public class FinalizableObject { public FinalizableObject() { Console.WriteLine("Instance initialized"); } ~FinalizableObject() { Console.WriteLine("Instance finalized"); } }
نُنشئ كائنًا من هذا النوع:
new FinalizableObject();
مما يُنتِج الخْرج التالي بالرغم مِن أننا لم نَستخدِم الكائن بَعْد:
<namespace>.FinalizableObject initialized
لن يُنْهَى الكائن حتى يَنتهي عَمَل البرنامج، والذي يؤدي في الواقع إلى إنهاء جميع الكائنات وتحرير مساحتها من قسم الكَوْمَة بالذاكرة. ومع ذلك يُمكنك استدعاء التابع Collect
لإجبار كَانِس المُهملات (Garbage Collector) على إجراء عملية كَنْس خلال لحظة معينة، كالتالي:
new FinalizableObject(); GC.Collect();
مما يُنتِج الخْرج التالي:
<namespace>.FinalizableObject initialized <namespace>.FinalizableObject finalized
أُنْهيت جميع الكائنات الميتة غير المُستخدَمة (dead objects)، وحُرِّرت مساحتها من قسم الكَوْمَة بالذاكرة بمجرد اِستدعاء كَانِس المُهملات.
مثال 2
بِفَرْض أن كلًا من النوعين FinalizableObject1
و FinalizableObject2
مُشتقَّين من النوع FinalizableObject
المُعرَّف مُسبقًا ويَرِثا نفس سُلوك طباعة رسائل التهيئة والإنهاء، نُنشِئ الكائنات التالية:
var obj1 = new FinalizableObject1(); var obj2 = new FinalizableObject2(); obj1 = null; // (1) GC.Collect(); // (2)
يكون الخْرج كالتالي:
<namespace>.FinalizableObject1 initialized <namespace>.FinalizableObject2 initialized <namespace>.FinalizableObject1 finalized
(1) لمّا أُسْنِدت القيمة الفارغة null
إلى المُتغيّر obj1
(كان يَحمِل مَرجِعا إلى الكائن من النوع FinalizableObject1
)، أصبح مِن غير الممكن -بطبيعة الحال- الولوج لهذا الكائن مرة أُخرى وبالتالي عُدّ ميتًا. ولذلك عندما اُستدعِي كَانِس المُهملات في وقت لاحق من تنفيذ الشيفرة (2)، أنهاه وحرَّر مساحته بقسم الكَوْمَة بالذاكرة، ويَظهَر ذلك جَلّيًا من خلال طباعة عبارة المُنْهِي (finalizer). على النقيض، ما يزال هناك مَرجِع للكائن من النوع FinalizableObject2
وبالتالي عُدّ حيًّا ولم يُنْهَ.
مثال 3
بِفَرْض أن النوع FinalizableObject
يَحمِل خاصية عَلّنية (public) من نفس نوعه تُسمَّى OtherObject
. ماذا سيحُدث لو كان هناك كائنين ميتين، يَحمِل كلًا منهما مَرجِعًا للآخر؟ كالتالي:
var obj1 = new FinalizableObject1(); var obj2 = new FinalizableObject2(); obj1.OtherObject = obj2; obj2.OtherObject = obj1; obj1 = null; // لم يعد هناك مَرجِع للكائن من النوع FinalizableObject1 obj2 = null; // لم يعد هناك مَرجِع للكائن من النوع FinalizableObject2 // لكن كلا منهما ما يزال يحمل مرجعا للآخر GC.Collect()
يكون الخْرج كالتالي:
<namespace>.FinalizedObject1 initialized <namespace>.FinalizedObject2 initialized <namespace>.FinalizedObject1 finalized <namespace>.FinalizedObject2 finalized
على الرغم من أن كِلاَ الكائنين يَحمِل مَرجِعا إلى الكائن الآخر، يَقوُم كانس المُهملات بإنهائهما ويُحرِّر مساحتهما من قسم الكَوْمَة بالذاكرة؛ وذلك لعدم وجود أي مَرجِع لهما ضِمْن كائن حيّ.
المَراجِع الضعيفة (Weak References)
المَراجِع الضعيفة WeakReference
هي -كأيّ مَرجِع- تُشير إلى مكان كائن مُعين بالذاكرة. لا يُعوِّل كَانِس المُهملات على وجود المَراجِع الضعيفة (weak references) عند تقديره لحالة الكائن من حيث كَوْنه حيًا أو ميتًا. وبالتالي، لا يَمنع وجود مَرجِع ضعيف إلى كائن معين كَانِس المُهملات من إنهاء هذا الكائن وتَحرير مساحته بالذاكرة، ومن هنا كان عَدّها ضعيفة (weak).
انظر المثال التالي:
var weak = new WeakReference<FinalizableObject>(new FinalizableObject()); GC.Collect();
يُنتِج الخْرج التالي:
<namespace>.FinalizableObject initialized <namespace>.FinalizableObject finalized
حَذَفَ كَانِس المُهملات -عند اِسْتِدْعائه- الكائن من قسم الكَوْمَة بالذاكرة على الرغم من وجود مَرجِعًا إليه من النوع WeakReference
داخل النطاق (scope). نَستنتِج من ذلك:
أولًا: مِن غيْر الآمن أن تَفترِض أن المساحة المُخصَّصة بقسم الكَوْمَة لكائن مُشار إليه بمَرجِع ضعيف لا تزال صالحة.
ثانيًا: عندما تحتاج إلى تحصيل (dereference) قيمة مَرجِع ضعيف، اِستخدِم التابع TryGetTarget
أولًا والذي يُعيد قيمة منطقية تُحدِّد إذا ما حُرِّرت مساحته بالذاكرة أم لا، ثم اُكتب شيفرة لمُعالجة كِلاَ الحالتين. كالتالي:
var target = new object(); var weak = new WeakReference<object>(target); target = null; // فحص ما إذا كان الكائن ما زال مُتاحًا if(weak.TryGetTarget(out target)) { // يُمكنك استخدام الكائن هنا } else { // لا ينبغي استخدام الكائن هنا }
تُوفِّر جميع إصدارات إطار عمل .NET نسخة غير مُعمَّمة من النوع WeakReference
والتي تَعمَل بنفس الطريقة. في المقابل، دُعِّمت النسخة المُعمَّمة منذ اصدار 4.5.
انظر المثال التالي للنسخة الغيْر مُعمَّمة:
var target = new object(); var weak = new WeakReference(target); target = null; if (weak.IsAlive) { target = weak.Target; // يُمكنك استخدام الكائن هنا } else { // لا ينبغي استخدام الكائن هنا }
التابع Dispose
والمنهيات (finalizers)
إذا أردت التأكد من تحرير الموارد (resources) التي يَستخدِمها كائن مُعين بمجرد أن يَخْرُج من النطاق ويُصبح من غير الممكن اِستخدَامه -خاصة إن كانت الموارد مُستَنزِفة للذاكرة أو غير مُدارة مثل التَعامُل مع الملفات فقد يُبلَّغ عن اعتراض عند محاولة فتح ملف للقراءة ولا يُغلَق مِقبَض الملف (file handle) كما يَنبغي-، صَرِّح عن كَوْن نوع الكائن من الواجهة IDisposable
والتي تحتوي على تابع وحيد هو Dispose
لا يَستقبِل أي مُعامِلات:
public interface IDisposable { Dispose(); }
ثم نفِّذ تابعها Dispose
داخل النوع المَذكور بحيث يكون هذا التابع مَسئولًا عن تحرير تلك الموارد.
بخلاف المُنْهِيات (finalizers) التي تُستَدعَى دائمًا عند انتهاء فترة حياة الكائن، يَقع عاتِق اِستِدعاء التابع Dispose
على المُبرمِج وهو ما ليس مَضْمُونًا. يُمكن للمُبرمِج اِستِدعاء التابع Dispose
صراحةً، كالمثال التالي:
private void SomeFunction() { // هيئ كائن يستهلك موارد مستنزفة للذاكرة var disposableObject = new ClassThatImplementsIDisposable(); // استدعي التابع Dispose disposableObject.Dispose(); // (1) }
(1): يَخرُج المُتغيّر disposableObject
من النطاق (scope). مع ذلك، لا يُمكننا الجزم بموعد إنهاء (finalize) الكائن لنفسه، ولذلك يَضمَن اِستِدعاء التابع Dispose
تحرير موارده المُستَنزِفة للذاكرة.
ربما تَستَدعِيها كذلك صراحةً داخل كُتلة finally
، كالمثال التالي:
StreamReader sr; string textFromFile; string filename = "SomeFile.txt"; try { sr = new StreamReader(filename); textFromFile = sr.ReadToEnd(); } finally { if (sr != null) sr.Dispose(); }
أو من خلال اِستخدَام عبارة using
والتي في الواقع يُفضَّل اِستخدَامها، وسيُحوِّلها المُصرِّف آليًا إلى نفس الشيفرة الموجودة بالأعلى، كالتالي:
string textFromFile; string filename = "SomeFile.txt"; using (StreamReader sr = new Streamreader(filename)) { textFromFile = sr.ReadToEnd(); }
يُعدّ التَعامُل مع الأنواع التي يَكون إطار العمل مسؤولًا عن تَنشِئة كائناتها مثال آخر. في هذه الحالة، عادة ما يُشتقّ النوع الجديد من نوع أساسي (base). مثلًا، عندما تُعْلِن عن نوع مُتَحكِم جديد (controller)، يَرِث من النوع الأساسي System.Web.Mvc.ControllerBase
. فإذا كان النوع الأساسي يُنفِّذ الواجهة IDisposable
، غالبًا ما يَعني ذلك أن اطار العمل سيَستَدعِي التابع Dispose
بطريقة سليمة، ولكن هذا ليس مَضْمُونًا.
لا يُعدّ التابع Dispose
بديلًا للمُنْهِي (finalizer)، لكن ينبغي أن تَستخدِم كليهما بحسب الغرض:
- يُحرِّر المُنْهِي الموارد -على أيّ حال- لتَجَنُّب حُدوث أي تَسرُّب للذاكرة (memory leaks).
-
يُحرِّر التابع
Dispose
الموارد بمجرد إنتهاء الحاجة إليها؛ لتخفيف الضغط على الذاكرة المُخصَّصة (memory allocation) عامةً.
استخدام النوع SafeHandle
لتغليف الموارد غير المُدارة
عند كتابة مُغلِّف (wrapper) لموارد غير مُدارة، اِحرص على أن يُشتقّ المُغلِّف من النوع SafeHandle
بدلًا من أن تُنفِّذ الواجهة IDisposable
أو تُنشِئ مُنْهِي (finalizer) بنفسك، حيث يُنفِّذ هذا النوع بالفعل الواجهة من أجلك، ويُهيِئ المُنْهِيات (finalizers) تهيئة مناسبة، كما يَضمَن تَّنفيذ شيفرة التحرير.
يجب أن يَكون النوع المُشتقّ من الصنف SafeHandle
صغيرًا وبسيطًا لتقليل احتمالية تَسريب المِقبَض (handle). يتأكد النوع SafeHandle
من تحرير أي موارد غيْر مُدارة حتى في حالة حدوث تَسريب لكائنات المُغلِّف.
انظر المثال التالي:
using System.Runtime.InteropServices; class MyHandle : SafeHandle { public override bool IsInvalid => handle == IntPtr.Zero; public MyHandle() : base(IntPtr.Zero, true) { } public MyHandle(int length) : this() { SetHandle(Marshal.AllocHGlobal(length)); } protected override bool ReleaseHandle() { Marshal.FreeHGlobal(handle); return true; } }
تنبيه: لا يَتعدَّى المثال بالأعلى كَوْنِه مجرد محاولة لاستعراض طريقة اِشتقاق SafeHandle
. وعليه وجب التنبيه أنه من العبث تَخصيص جزء من الذاكرة بهذه الطريقة.
تحرير الكائنات وإنهائها بشكل سليم
يَستهدِف كلًا من التابع Dispose
والمُنْهِيات (finalizer) ظرفًا مختلفًا. ولذلك، من الضروري للأنواع التي تَستخدِم موارد مُستَنزِفة للذاكرة أن تُنفِّذ كلتا الطريقتين. وبالنتيجة، نحصل على نوع يُمكنه التعامُل المُلائم مع كِلاَ المَوقِفيّن المُحتملين:
- اِستدعاء المُنْهِي فقط.
- اِستدعاء التابع Dispose أولًا ثم اِستدعاء المُنْهِي فيما بعد.
أحد الحلول هو كتابة شيفرة التنظيف (cleanup) بطريقة قابلة للتنفيذ أكثر من مرة بدون وجود اختلاف بالنتيجة. تَعتمِد تلك القابلية على طبيعة عملية التنظيف نفسها، على سبيل المثال:
-
لا تُحدِث محاولة إغلاق اتصال بقاعدة بيانات (connection) تم إغلاقه مُسبقّا فرقًا.
-
قد يؤدي تحديث عَداد إلى نتائج خاطئة عند اِستدعاء الشيفرة مرتين بدلًا من مرة واحدة.
حل آخر هو التأكد من تَّنفيذ شيفرة التنظيف (cleanup) مرة واحدة فقط بغض النظر عن السياق الخارجي، وهو ما يُمكن عادةً تحقيقه باستخدام مُتغيّر راية (flag) مُخصَّص لهذا الغرض. كالتالي:
public class DisposableFinalizable1: IDisposable { private bool disposed = false; ~DisposableFinalizable1() { Cleanup(); } public void Dispose() { Cleanup(); } private void Cleanup() { if(!disposed) { // ضمن الشيفرة الفعلية المسئولة عن تحرير الموارد هنا disposed = true; } } }
كذلك يمكن تحقيقه باِستدعاء التابع SuppressFinalize()
في حالة اِستدعاء التابع Dispose
. يُوفِّر كانس المُهملات هذا التابع الذي يَسمَح بتخطي تَّنفيذ المُنْهِي (finalizer)، كالتالي:
public class DisposableFinalizable2 : IDisposable { ~DisposableFinalizable2() { Cleanup(); } public void Dispose() { Cleanup(); GC.SuppressFinalize(this); } private void Cleanup() { // ضمن الشيفرة الفعلية المسئولة عن تحرير الموارد هنا } }
التخزين المؤقت Caching
يُوفِّر النوع MemoryCache
بإطار عمل .NET
العديد من التوابع لتخزين البيانات بالذاكرة (memory).
إضافة عنصر باستخدام التابع Set
يُستخدَم التابع Set
لإضافة مُدخَل إلى الذاكرة المخبئية (cache)، حيث يَستقبَل مُعامل من النوع CacheItem
يَحمِل كلًا من مفتاح وقيمة المُدخَل.
private static bool SetToCache() { string key = "Cache_Key"; string value = "Cache_Value"; // احصل على مَرجِع لنُسخة النوع MemoryCache الافتراضية var cacheContainer = MemoryCache.Default; var policy = new CacheItemPolicy() { AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(DEFAULT_CACHE_EXPIRATION_MINUTES) }; var itemToCache = new CacheItem(key, value); cacheContainer.Set(itemToCache, policy); }
لاحظ استخدام الصنف CacheItemPolicy
لضبط توقيت انتهاء صلاحية المُدخَل ومَسحُه من الذاكرة المخبئية.
جلب أو إضافة عنصر باستخدام التابع AddOrGetExisting
يَجلِب التابع AddOrGetExisting
قيمة المفتاح المُمرَّر إليه من الذاكرة المخبئية (cache) في حالة وجوده. أما إذا لم يُكن موجودًا، فإنه سيَستخدِم المُفوِّض valueFetchFactory
المُمرَّر إليه لجَلْب قيمة يُخزِنها بالذاكرة كقيمة للمفتاح المحدَّد ثم يُعيدها.
public static TValue GetExistingOrAdd<TValue>(string key, double minutesForExpiration, Func<TValue> valueFetchFactory) { try { // سيتم تقييم التهيئة المرجأة للنوع فقط في حالة عدم وجود العنصر المطلوب بالذاكرة المخبئية var newValue = new Lazy<TValue>(valueFetchFactory); CacheItemPolicy policy = new CacheItemPolicy() { AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(minutesForExpiration) }; // يعيد العنصر من الذاكرة المخبئية إذا كان موجودًا أو يُضيفه في حالة عدم وجوده var cachedItem = _cacheContainer.AddOrGetExisting(key, newValue, policy) as Lazy<TValue>; return (cachedItem ?? newValue).Value; } catch (Exception excep) { return default(TValue); } }
ترجمة -وبتصرف- للفصول Memory management و Garbage Collection و System.Runtime.Caching.MemoryCache من كتاب .NET Framework Notes for Professionals
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.