[DotNetBook] أحداث الاستثناء وكيفية الحصول على StackOverflow و ExecutionEngineException من البداية


أحداث الاستثناء


في الحالة العامة ، لا نعرف دائمًا الاستثناءات التي ستحدث في برامجنا لأننا دائمًا ما نستخدم شيئًا مكتوبًا بواسطة أشخاص آخرين وهو موجود في أنظمة فرعية ومكتبات أخرى. ليس فقط يمكن أن يكون هناك مجموعة متنوعة من المواقف في التعليمات البرمجية الخاصة بك ، في التعليمات البرمجية للمكتبات الأخرى ، هناك أيضًا العديد من المشاكل المرتبطة بتنفيذ التعليمات البرمجية في المجالات المعزولة. وفقط في هذه الحالة ، سيكون من المفيد للغاية أن تكون قادرًا على تلقي البيانات حول تشغيل التعليمات البرمجية المعزولة. بعد كل شيء ، قد يكون الموقف حقيقيًا تمامًا عندما يعترض رمز الطرف الثالث جميع الأخطاء دون استثناء ، ويغرق fault كتلة:


 try { // ... } catch { // do nothing, just to make code call more safe } 

في مثل هذه الحالة ، قد يتبين أن تنفيذ التعليمات البرمجية لم يعد آمنًا كما يبدو ، ولكن ليس لدينا أي رسائل حول أي مشاكل. الخيار الثاني هو عندما يمنع التطبيق بعض الاستثناءات ، حتى القانونية. والنتيجة - الاستثناء التالي في مكان عشوائي سيؤدي إلى تعطل التطبيق في المستقبل بسبب خطأ عشوائي على ما يبدو. هنا أود أن أحصل على فكرة عن خلفية هذا الخطأ. ما هو مسار الأحداث التي أدت إلى هذا الوضع. وتتمثل إحدى الطرق لجعل ذلك ممكنًا في استخدام أحداث إضافية تتعلق بمواقف استثنائية: AppDomain.FirstChanceException و AppDomain.UnhandledException .


ملاحظة


لم يتم تحديث الفصل المنشور على حبري ، وربما يكون قديمًا بالفعل. وبالتالي ، يرجى الرجوع إلى النص الأصلي للحصول على نص أحدث:



في الواقع ، عندما "تطرح استثناءً" ، يتم استدعاء الطريقة المعتادة لبعض الأنظمة الفرعية الداخلية للرمي ، والتي تقوم بداخلها بتنفيذ العمليات التالية:


  • يلقي AppDomain.FirstChanceException
  • يبحث عن مرشحات مطابقة في سلسلة المعالجات
  • يجعل المعالج يلف المكدس مسبقًا للإطار المطلوب.
  • إذا لم يتم العثور على معالج ، يتم AppDomain.UnhandledException ، مما يؤدي إلى تعطل مؤشر الترابط الذي حدث فيه الاستثناء.

يجب على المرء أن يقوم على الفور بالحجز عند الإجابة على سؤال يعذب الكثير من العقول: هل من الممكن إلغاء الاستثناء الذي حدث في الكود غير المنضبط الذي يتم تنفيذه في المجال المعزول دون كسر هذا الخيط الذي تم طرح هذا الاستثناء فيه؟ الجواب موجز وبسيط: لا. إذا لم يتم اكتشاف استثناء في النطاق الكامل للطرق المطلوبة ، فلا يمكن التعامل معه من حيث المبدأ. خلاف ذلك ، ينشأ موقف غريب: إذا استخدمنا AppDomain.FirstChanceException للتعامل مع استثناء (نوع من catch الاصطناعي) ، فما هو الإطار الذي يجب أن يتراجع فيه رصة الخيط؟ كيفية تعيين هذا كجزء من قواعد .NET CLR؟ مستحيل. إنه ببساطة غير ممكن. الشيء الوحيد الذي يمكننا القيام به هو تسجيل المعلومات الواردة للبحث في المستقبل.


الشيء الثاني للحديث عن "الشاطئ" هو لماذا لم يتم عرض هذه الأحداث في Thread ، ولكن في AppDomain . بعد كل شيء ، إذا اتبعت المنطق ، تنشأ استثناءات أين؟ في تدفق تنفيذ الأمر. على سبيل المثال في الواقع Thread . فلماذا يعاني المجال من مشاكل؟ الجواب بسيط للغاية: ما هي المواقف التي تم AppDomain.UnhandledException و AppDomain.UnhandledException ؟ من بين أمور أخرى - لإنشاء صناديق رمل للمكونات الإضافية. على سبيل المثال للحالات التي يكون فيها AppDomain معين تم تكوينه لـ PartialTrust. يمكن أن يحدث أي شيء داخل AppDomain هذا: يمكن إنشاء سلاسل الرسائل في أي لحظة ، أو يمكن استخدام سلاسل الرسائل الموجودة من ThreadPool. ثم اتضح أننا ، خارج هذه العملية (لم نكتب هذا الرمز) ، لا يمكننا الاشتراك في أحداث التدفقات الداخلية. فقط لأنه ليس لدينا فكرة عن التدفقات التي تم إنشاؤها هناك. لكننا نضمن أن يكون لدينا AppDomain الذي ينظم وضع الحماية والروابط التي لدينا.


لذا ، في الواقع ، تم FirstChanceExecption إقليميين: حدث شيء لم يكن من المفترض ( FirstChanceExecption ) و "كل شيء سيئ" ، لم يعالج أحد الاستثناء: لم يتم توفيره. لذلك ، فإن تدفق تنفيذ الأمر لا معنى له وسيتم شحنه ( Thread ).


ما الذي يمكن الحصول عليه من خلال هذه الأحداث ولماذا يكون من السيء أن يتجاوز المطورون هذه الأحداث؟


AppDomain.FirstChanceException


هذا الحدث ذو طبيعة إعلامية بحتة ولا يمكن "معالجته". وتتمثل مهمتها في إعلامك بحدوث استثناء في هذا المجال وستبدأ معالجته بواسطة رمز التطبيق بعد معالجة الحدث. يحمل تنفيذه بضع ميزات يجب تذكرها أثناء تصميم المعالج.


ولكن دعنا أولاً نلقي نظرة على مثال اصطناعي بسيط عن معالجته:


 void Main() { var counter = 0; AppDomain.CurrentDomain.FirstChanceException += (_, args) => { Console.WriteLine(args.Exception.Message); if(++counter == 1) { throw new ArgumentOutOfRangeException(); } }; throw new Exception("Hello!"); } 

ما الذي يميز هذا الرمز؟ أينما طرح بعض التعليمات البرمجية استثناءً ، فإن أول ما يحدث هو تسجيله في وحدة التحكم. على سبيل المثال حتى إذا نسيت أو لا يمكنك تصور التعامل مع نوع من الاستثناء ، فسيظل يظهر في سجل الأحداث الذي تنظمه. والثاني هو حالة غريبة إلى حد ما لرمي استثناء داخلي. الشيء هو أنه داخل معالج FirstChanceException لا يمكنك فقط رمي ورمي استثناء آخر. بدلاً من ذلك ، حتى هذا: داخل معالج FirstChanceException ، لا يمكنك طرح أي استثناء على الأقل. إذا قمت بذلك ، فهناك حدثان محتملان. في البداية ، إذا لم تكن هناك if(++counter == 1) ، فسوف نحصل على FirstChanceException لانهائي لـ ArgumentOutOfRangeException الجديد تمامًا. ماذا يعني هذا؟ هذا يعني أنه في مرحلة معينة سنحصل على StackOverflowException : طرح throw new Exception("Hello!") FirstChanceException أسلوب CLR Throw ، الذي يلقي FirstChanceException ، والذي يرمي Throw بالفعل لـ ArgumentOutOfRangeException ثم يتكرر. الخيار الثاني - دافعنا عن أنفسنا بعمق العودية باستخدام الشرط counter . على سبيل المثال في هذه الحالة ، نرمي استثناء مرة واحدة فقط. والنتيجة أكثر من غير متوقعة: نحصل على استثناء يعمل بالفعل داخل تعليمات Throw . وما هو الأنسب لهذا النوع من الأخطاء؟ وفقًا لـ ECMA-335 ، إذا تم طرح التعليمات في استثناء ، فيجب طرح ExecutionEngineException ! لكننا لسنا قادرين على التعامل مع هذا الوضع الاستثنائي. يؤدي إلى تعطل كامل من التطبيق. ما هي خيارات المعالجة الآمنة لدينا؟


أول شيء يتبادر إلى الذهن هو تعيين كتلة try-catch على التعليمات البرمجية لمعالج FirstChanceException :


 void Main() { var fceStarted = false; var sync = new object(); EventHandler<FirstChanceExceptionEventArgs> handler; handler = new EventHandler<FirstChanceExceptionEventArgs>((_, args) => { lock (sync) { if (fceStarted) { //     - ,        -      , //   try  . Console.WriteLine($"FirstChanceException inside FirstChanceException ({args.Exception.GetType().FullName})"); return; } fceStarted = true; try { //     . ,   Console.WriteLine(args.Exception.Message); throw new ArgumentOutOfRangeException(); } catch (Exception exception) { //       Console.WriteLine("Success"); } finally { fceStarted = false; } } }); AppDomain.CurrentDomain.FirstChanceException += handler; try { throw new Exception("Hello!"); } finally { AppDomain.CurrentDomain.FirstChanceException -= handler; } } OUTPUT: Hello! Specified argument was out of the range of valid values. FirstChanceException inside FirstChanceException (System.ArgumentOutOfRangeException) Success !Exception: Hello! 

على سبيل المثال من ناحية ، لدينا رمز للتعامل مع حدث FirstChanceException ، ومن ناحية أخرى ، لدينا رمز إضافي للتعامل مع الاستثناءات في FirstChanceException نفسها. ومع ذلك ، يجب أن تكون تقنيات التسجيل لكلا الحالتين مختلفة. إذا كان تسجيل معالجة الحدث يمكن أن يسير كما تريد ، فيجب أن FirstChanceException معالجة خطأ منطق معالجة FirstChanceException دون استثناء من حيث المبدأ. الشيء الثاني الذي لاحظته على الأرجح هو المزامنة بين سلاسل المحادثات. قد يثير هذا السؤال: لماذا هنا إذا تم طرح أي استثناء في أي موضوع ، مما يعني أن FirstChanceException يجب أن يكون آمنًا لمؤشر الترابط. ومع ذلك ، كل شيء ليس مبتهجًا جدًا. FirstChanceException لدينا في AppDomain. وهذا يعني أنه يحدث لأي مؤشر ترابط يبدأ في مجال معين. على سبيل المثال إذا كان لدينا مجال يتم فيه بدء العديد من FirstChanceException ، FirstChanceException أن يتم FirstChanceException بالتوازي. وهذا يعني أننا بحاجة إلى حماية أنفسنا بطريقة ما مع المزامنة: على سبيل المثال ، باستخدام lock .


الطريقة الثانية هي محاولة تحويل المعالجة إلى مؤشر ترابط مجاور ينتمي إلى مجال تطبيق مختلف. ومع ذلك ، تجدر الإشارة إلى أنه مع مثل هذا التنفيذ ، يجب أن نبني مجالًا مخصصًا خصيصًا لهذه المهمة حتى لا يعمل حتى يمكن للتدفقات الأخرى التي تعمل أن تضع هذا المجال:


 static void Main() { using (ApplicationLogger.Go(AppDomain.CurrentDomain)) { throw new Exception("Hello!"); } } public class ApplicationLogger : MarshalByRefObject { ConcurrentQueue<Exception> queue = new ConcurrentQueue<Exception>(); CancellationTokenSource cancellation; ManualResetEvent @event; public void LogFCE(Exception message) { queue.Enqueue(message); } private void StartThread() { cancellation = new CancellationTokenSource(); @event = new ManualResetEvent(false); var thread = new Thread(() => { while (!cancellation.IsCancellationRequested) { if (queue.TryDequeue(out var exception)) { Console.WriteLine(exception.Message); } Thread.Yield(); } @event.Set(); }); thread.Start(); } private void StopAndWait() { cancellation.Cancel(); @event.WaitOne(); } public static IDisposable Go(AppDomain observable) { var dom = AppDomain.CreateDomain("ApplicationLogger", null, new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.BaseDirectory, }); var proxy = (ApplicationLogger)dom.CreateInstanceAndUnwrap(typeof(ApplicationLogger).Assembly.FullName, typeof(ApplicationLogger).FullName); proxy.StartThread(); var subscription = new EventHandler<FirstChanceExceptionEventArgs>((_, args) => { proxy.LogFCE(args.Exception); }); observable.FirstChanceException += subscription; return new Subscription(() => { observable.FirstChanceException -= subscription; proxy.StopAndWait(); }); } private class Subscription : IDisposable { Action act; public Subscription (Action act) { this.act = act; } public void Dispose() { act(); } } } 

في هذه الحالة ، يكون التعامل مع FirstChanceException آمنًا قدر الإمكان: في سلسلة FirstChanceException المجاورة التي تنتمي إلى المجال المجاور. في هذه الحالة ، لا يمكن أن تؤدي الأخطاء في معالجة الرسالة إلى إسقاط سير عمل التطبيق. بالإضافة إلى ذلك ، يمكنك الاستماع بشكل منفصل إلى UnhandledException لمجال تسجيل الرسائل: لن تؤدي الأخطاء الفادحة أثناء التسجيل إلى إسقاط التطبيق بالكامل.


AppDomain.UnhandledException


الرسالة الثانية التي يمكننا التقاطها والتي تتعامل مع معالجة الاستثناء هي AppDomain.UnhandledException . هذه الرسالة أخبار سيئة للغاية بالنسبة لنا لأنها تعني أنه لم يكن هناك أي شخص يمكنه العثور على طريقة للتعامل مع الخطأ في سلسلة محادثات معينة. أيضا ، إذا حدث مثل هذا الموقف ، كل ما يمكننا القيام به هو "مسح" عواقب مثل هذا الخطأ. على سبيل المثال بأي شكل من الأشكال لتنظيف الموارد التي تنتمي إلى هذا الدفق فقط إذا تم إنشاء أي. ومع ذلك ، فإن الوضع الأفضل هو التعامل مع الاستثناءات أثناء وجودها في جذر السلاسل دون حظر الخيط. على سبيل المثال وضعت أساسا try-catch . دعونا نحاول النظر في مدى ملاءمة هذا السلوك.


لنفترض أن لدينا مكتبة تحتاج إلى إنشاء سلاسل رسائل وتنفيذ نوع من المنطق في هذه المواضيع. نحن ، كمستخدمين لهذه المكتبة ، مهتمون فقط بضمان مكالمات API وكذلك تلقي رسائل الخطأ. إذا كانت المكتبة سوف تتعطل التدفقات دون إخطار عنها ، فهذا لا يمكن أن يساعدنا كثيرًا. علاوة على ذلك ، سيؤدي انهيار الدفق إلى رسالة AppDomain.UnhandledException ، حيث لا توجد معلومات حول أي دفق معين يقع على جانبه. إذا كنا نتحدث عن رمزنا ، فمن غير المرجح أن يكون التدفق المتعطل مفيدًا لنا. على أي حال ، لم أكن أفي بالحاجة لذلك. مهمتنا هي معالجة الأخطاء بشكل صحيح ، وإرسال معلومات حول حدوثها إلى سجل الأخطاء وإنهاء التدفق بشكل صحيح. على سبيل المثال قم بلف الطريقة التي يبدأ بها الخيط في try-catch :


  ThreadPool.QueueUserWorkitem(_ => { using(Disposables aggregator = ...){ try { // do work here, plus: aggregator.Add(subscriptions); aggregator.Add(dependantResources); } catch (Exception ex) { logger.Error(ex, "Unhandled exception"); } } }); 

في مثل هذا المخطط ، نحصل على ما نحتاجه: من ناحية ، لن نكسر التيار. من ناحية أخرى ، قم بتنظيف الموارد المحلية بشكل صحيح إذا تم إنشاؤها. حسنًا ، في الملحق - ننظم تسجيل الخطأ الذي تم تلقيه. لكن انتظر ، تقول. بطريقة أو بأخرى ، ارتدت مشكلة حدث AppDomain.UnhandledException . هل حقا ليس من الضروري على الإطلاق؟ من الضروري. ولكن فقط للإبلاغ أننا نسينا أن نلف بعض الخيوط في try-catch بكل المنطق الضروري. مع كل شيء: مع تسجيل وتنقية الموارد. خلاف ذلك ، سيكون من الخطأ تمامًا: أخذ وإلغاء جميع الاستثناءات ، كما لو لم تكن موجودة على الإطلاق.


رابط للكتاب كله



Source: https://habr.com/ru/post/ar419929/


All Articles