[DotNetBook] Ausnahmeereignisse und wie Sie StackOverflow und ExecutionEngineException von Grund auf neu erstellen


Ausnahmeereignisse


Im Allgemeinen kennen wir die Ausnahmen, die in unseren Programmen auftreten, nicht immer, da wir fast immer etwas verwenden, das von anderen Personen geschrieben wurde und das sich in anderen Subsystemen und Bibliotheken befindet. Es kann nicht nur eine Vielzahl von Situationen in Ihrem eigenen Code geben, sondern auch im Code anderer Bibliotheken, und es gibt auch viele Probleme, die mit der Ausführung von Code in isolierten Domänen verbunden sind. Und gerade in diesem Fall wäre es äußerst nützlich, Daten über den Betrieb von isoliertem Code empfangen zu können. Schließlich kann eine Situation durchaus real sein, wenn Code von Drittanbietern ausnahmslos alle Fehler abfängt und ihren fault Block übertönt:


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

In einer solchen Situation kann sich herausstellen, dass die Codeausführung nicht mehr so ​​sicher ist, wie es aussieht, aber wir haben keine Meldungen über Probleme. Die zweite Option ist, wenn die Anwendung einige, sogar rechtliche Ausnahmen unterdrückt. Und das Ergebnis - die folgende Ausnahme an einer zufälligen Stelle führt dazu, dass die Anwendung in Zukunft aufgrund eines scheinbar zufälligen Fehlers abstürzt. Hier möchte ich eine Vorstellung davon haben, was der Hintergrund dieses Fehlers war. Wie verliefen die Ereignisse zu dieser Situation? Eine Möglichkeit, dies zu ermöglichen, besteht darin, zusätzliche Ereignisse zu verwenden, die sich auf Ausnahmesituationen beziehen: AppDomain.FirstChanceException und AppDomain.UnhandledException .


Hinweis


Das auf Habré veröffentlichte Kapitel ist nicht aktualisiert und wahrscheinlich bereits etwas veraltet. Wenden Sie sich daher für einen neueren Text dem Original zu:



Wenn Sie "eine Ausnahme auslösen", wird die übliche Methode eines internen Throw Subsystems aufgerufen, das in sich die folgenden Operationen ausführt:


  • AppDomain.FirstChanceException
  • Sucht nach passenden Filtern in der Handlerkette
  • Bewirkt, dass der Handler den Stapel auf den gewünschten Frame vorrollt.
  • Wenn kein Handler gefunden wurde, wird eine AppDomain.UnhandledException , die den Thread zum Absturz AppDomain.UnhandledException , in dem die Ausnahme aufgetreten ist.

Man sollte sofort eine Reservierung vornehmen, wenn man eine Frage beantwortet, die viele Köpfe quält: Ist es möglich, die Ausnahme, die in dem unkontrollierten Code aufgetreten ist, der in der isolierten Domäne ausgeführt wird, irgendwie abzubrechen, ohne dadurch den Thread zu unterbrechen, in den diese Ausnahme ausgelöst wurde? Die Antwort ist kurz und einfach: Nein. Wenn eine Ausnahme nicht für alle aufgerufenen Methoden erfasst wird, kann sie grundsätzlich nicht behandelt werden. Andernfalls tritt eine seltsame Situation auf: Wenn wir eine AppDomain.FirstChanceException um eine AppDomain.FirstChanceException (eine Art synthetischen catch ) zu behandeln, auf welchen Frame sollte der Thread-Stapel dann zurückgesetzt werden? Wie kann ich dies als Teil der .NET CLR-Regeln festlegen? Auf keinen Fall. Es ist einfach nicht möglich. Das einzige, was wir tun können, ist, die erhaltenen Informationen für zukünftige Forschungen aufzuzeichnen.


Das zweite, AppDomain an Land sprechen sollte, ist, warum diese Ereignisse nicht bei Thread , sondern bei AppDomain . Wenn Sie der Logik folgen, entstehen schließlich Ausnahmen, wo? Im Ablauf der Befehlsausführung. Das heißt, eigentlich Thread . Warum hat die Domain Probleme? Die Antwort ist sehr einfach: Für welche Situationen wurden AppDomain.FirstChanceException und AppDomain.UnhandledException ? Unter anderem - um Sandboxen für Plugins zu erstellen. Das heißt, für Situationen, in denen eine bestimmte AppDomain für PartialTrust konfiguriert ist. Innerhalb dieser AppDomain kann alles passieren: Dort können jederzeit Threads erstellt oder vorhandene Threads aus ThreadPool verwendet werden. Dann stellt sich heraus, dass wir außerhalb dieses Prozesses (wir haben diesen Code nicht geschrieben) die Ereignisse interner Flows nicht abonnieren können. Nur weil wir keine Ahnung haben, welche Flüsse dort erzeugt wurden. Wir haben jedoch garantiert eine AppDomain , die die Sandbox und den Link, zu dem wir haben, organisiert.


Tatsächlich werden uns also zwei regionale Ereignisse zur Verfügung gestellt: etwas ist passiert, das nicht angenommen wurde ( FirstChanceExecption ) und "alles ist schlecht", niemand hat die Ausnahme behandelt: Es wurde nicht bereitgestellt. Daher ist der Ablauf der Befehlsausführung nicht sinnvoll und es wird ( Thread ) ausgeliefert.


Was kann durch diese Ereignisse erreicht werden und warum ist es schlecht, dass Entwickler diese Ereignisse umgehen?


AppDomain.FirstChanceException


Dieses Ereignis ist von Natur aus rein informativer Natur und kann nicht „verarbeitet“ werden. Seine Aufgabe ist es, Sie zu benachrichtigen, dass innerhalb dieser Domäne eine Ausnahme aufgetreten ist, und sie wird nach der Verarbeitung des Ereignisses vom Anwendungscode verarbeitet. Die Ausführung enthält einige Funktionen, die beim Entwurf des Prozessors berücksichtigt werden müssen.


Aber schauen wir uns zuerst ein einfaches synthetisches Beispiel für seine Verarbeitung an:


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

Was ist an diesem Code bemerkenswert? Wenn ein Code eine Ausnahme auslöst, wird er zuerst in der Konsole protokolliert. Das heißt, Selbst wenn Sie eine Ausnahme vergessen oder sich nicht vorstellen können, wird sie dennoch in dem von Ihnen organisierten Ereignisprotokoll angezeigt. Die zweite ist eine etwas seltsame Bedingung für das Auslösen einer internen Ausnahme. Die Sache ist, dass Sie im FirstChanceException Handler nicht einfach eine weitere Ausnahme FirstChanceException können. Vielmehr auch dies: Im FirstChanceException-Handler können Sie zumindest keine Ausnahme auslösen. In diesem Fall gibt es zwei mögliche Ereignisse. Wenn es keine if(++counter == 1) Bedingung if(++counter == 1) gäbe, würden wir zunächst eine unendliche FirstChanceException für eine FirstChanceException ArgumentOutOfRangeException . Was bedeutet das? Dies bedeutet, dass wir zu einem bestimmten Zeitpunkt eine StackOverflowException : throw new Exception("Hello!") FirstChanceException CLR Throw-Methode aus, die FirstChanceException , die Throw bereits für ArgumentOutOfRangeException auslöst und dann rekursiv. Die zweite Option - wir haben uns durch die Tiefe der Rekursion unter Verwendung der counter verteidigt. Das heißt, In diesem Fall wird eine Ausnahme nur einmal ausgelöst. Das Ergebnis ist mehr als unerwartet: Wir erhalten eine Ausnahme, die tatsächlich innerhalb der Throw Anweisung funktioniert. Und was ist für diese Art von Fehler am besten geeignet? Laut ECMA-335 muss eine ExecutionEngineException ausgelöst werden, wenn eine Anweisung in eine Ausnahme ausgelöst wurde! Wir sind jedoch nicht in der Lage, mit dieser Ausnahmesituation umzugehen. Dies führt zu einem vollständigen Absturz der Anwendung. Welche sicheren Verarbeitungsoptionen haben wir?


Das erste, was FirstChanceException in den Sinn kommt, ist das Setzen eines try-catch Blocks für den gesamten Code des 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! 

Das heißt, Einerseits haben wir den Code für die Behandlung des FirstChanceException Ereignisses und andererseits haben wir zusätzlichen Code für die Behandlung von Ausnahmen in der FirstChanceException selbst. Die Protokollierungstechniken für beide Situationen sollten jedoch unterschiedlich sein. Wenn die Ereignisverarbeitungsprotokollierung beliebig verlaufen kann, sollte die FirstChanceException der FirstChanceException Verarbeitungslogik grundsätzlich ausnahmslos erfolgen. Das zweite, was Sie wahrscheinlich bemerkt haben, ist die Synchronisation zwischen Threads. Dies kann die Frage aufwerfen: Warum ist es hier, wenn in einem Thread eine Ausnahme ausgelöst wird, was bedeutet, dass FirstChanceException sein sollte. Es ist jedoch nicht alles so fröhlich. FirstChanceException wir bei AppDomain. Dies bedeutet, dass es für jeden Thread auftritt, der in einer bestimmten Domäne gestartet wurde. Das heißt, Wenn wir eine Domäne haben, in der mehrere Threads gestartet werden, kann FirstChanceException parallel geschaltet werden. Und das bedeutet, dass wir uns irgendwie durch Synchronisation schützen müssen: zum Beispiel durch lock .


Die zweite Möglichkeit besteht darin, die Verarbeitung auf einen benachbarten Thread umzuleiten, der zu einer anderen Anwendungsdomäne gehört. Es ist jedoch erwähnenswert, dass wir bei einer solchen Implementierung eine dedizierte Domäne speziell für diese Aufgabe erstellen müssen, damit dies nicht funktioniert, damit andere funktionierende Flows diese Domäne platzieren können:


 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(); } } } 

In diesem Fall ist die Behandlung von FirstChanceException so sicher wie möglich: im benachbarten Thread, der zur benachbarten Domäne gehört. In diesem Fall können Fehler bei der Verarbeitung einer Nachricht die Anwendungsworkflows nicht beeinträchtigen. Außerdem können Sie die UnhandledException der Nachrichtenprotokollierungsdomäne separat abhören: Schwerwiegende Fehler während der Protokollierung führen nicht zum Herunterfahren der gesamten Anwendung.


AppDomain.UnhandledException


Die zweite Nachricht, die wir abfangen können und die sich mit der Ausnahmebehandlung befasst, ist AppDomain.UnhandledException . Diese Nachricht ist eine sehr schlechte Nachricht für uns, da es bedeutet, dass es niemanden gab, der einen Weg finden konnte, den Fehler in einem bestimmten Thread zu behandeln. Wenn eine solche Situation eingetreten ist, können wir nur die Konsequenzen eines solchen Fehlers "klären". Das heißt, in irgendeiner Weise, um Ressourcen zu bereinigen, die nur zu diesem Stream gehören, falls welche erstellt wurden. Eine noch bessere Situation ist es jedoch, Ausnahmen an der Wurzel der Threads zu behandeln, ohne den Thread zu blockieren. Das heißt, im Wesentlichen try-catch . Versuchen wir, die Angemessenheit dieses Verhaltens zu prüfen.


Angenommen, wir haben eine Bibliothek, die Threads erstellen und eine Art Logik in diesen Threads implementieren muss. Wir als Benutzer dieser Bibliothek sind nur daran interessiert, API-Aufrufe zu garantieren und Fehlermeldungen zu erhalten. Wenn die Bibliothek Streams zum Absturz bringt, ohne darüber informiert zu werden, kann uns dies nicht viel helfen. Darüber hinaus führt der Zusammenbruch des Streams zu einer AppDomain.UnhandledException Nachricht, in der keine Informationen darüber enthalten sind, welcher bestimmte Stream auf seiner Seite liegt. Wenn wir über unseren Code sprechen, ist es auch unwahrscheinlich, dass ein abstürzender Stream für uns nützlich ist. Auf jeden Fall habe ich die Notwendigkeit dafür nicht erfüllt. Unsere Aufgabe ist es, Fehler korrekt zu verarbeiten, Informationen über ihr Auftreten an das Fehlerprotokoll zu senden und den Ablauf korrekt zu beenden. Das heißt, Schließen Sie im Wesentlichen die Methode ein, mit der der Thread in try-catch beginnt:


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

In einem solchen Schema bekommen wir, was wir brauchen: Einerseits werden wir den Strom nicht brechen. Bereinigen Sie andererseits lokale Ressourcen korrekt, wenn sie erstellt wurden. Nun, im Anhang - wir organisieren die Protokollierung des empfangenen Fehlers. Aber warte, sagst du? Irgendwie haben Sie das Problem des Ereignisses AppDomain.UnhandledException . Ist es wirklich überhaupt nicht nötig? Es ist notwendig. Aber nur um zu informieren, dass wir vergessen haben, einige Threads mit der notwendigen Logik in try-catch zu verpacken. Mit allem: mit Protokollierung und Reinigung von Ressourcen. Andernfalls ist es völlig falsch: Nehmen Sie alle Ausnahmen und löschen Sie sie, als ob sie überhaupt nicht da wären.


Link zum ganzen Buch



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


All Articles