
Événements d'exception
Dans le cas général, nous ne connaissons pas toujours les exceptions qui se produiront dans nos programmes, car nous utilisons presque toujours quelque chose qui est écrit par d'autres personnes et qui se trouve dans d'autres sous-systèmes et bibliothèques. Non seulement il peut y avoir une variété de situations dans votre propre code, dans le code d'autres bibliothèques, il y a aussi de nombreux problèmes associés à l'exécution de code dans des domaines isolés. Et juste dans ce cas, il serait extrêmement utile de pouvoir recevoir des données sur le fonctionnement du code isolé. Après tout, une situation peut être bien réelle lorsque du code tiers intercepte toutes les erreurs sans exception, noyant leur fault
bloc:
try { // ... } catch { // do nothing, just to make code call more safe }
Dans une telle situation, il peut s'avérer que l'exécution du code n'est plus aussi sûre qu'elle en a l'air, mais nous n'avons aucun message concernant des problèmes. La deuxième option est lorsque l'application supprime une exception, même légale. Et le résultat - l'exception suivante dans un endroit aléatoire entraînera un plantage de l'application dans un certain avenir à partir d'une erreur apparemment aléatoire. Ici, je voudrais avoir une idée de l'arrière-plan de cette erreur. Quel est le cours des événements a conduit à cette situation. Et une façon de rendre cela possible consiste à utiliser des événements supplémentaires liés à des situations exceptionnelles: AppDomain.FirstChanceException
et AppDomain.UnhandledException
.
Remarque
Le chapitre publié sur Habré n'est pas mis à jour et, probablement, est un peu dépassé. Et par conséquent, veuillez vous tourner vers l'original pour un texte plus récent:

En fait, lorsque vous "lancez une exception", la méthode habituelle d'un sous-système Throw
interne est appelée, qui à l'intérieur d'elle-même effectue les opérations suivantes:
- Lève
AppDomain.FirstChanceException
- Recherche les filtres correspondants dans la chaîne des gestionnaires
- Force le gestionnaire à pré-rouler la pile dans le cadre souhaité.
- Si aucun gestionnaire n'a été trouvé, une
AppDomain.UnhandledException
est AppDomain.UnhandledException
, ce qui bloque le thread dans lequel l'exception s'est produite.
Il faut immédiatement faire une réserve en répondant à une question qui tourmente de nombreux esprits: est-il possible d'annuler en quelque sorte l'exception qui s'est produite dans le code non contrôlé qui est exécuté dans le domaine isolé sans rompre ainsi le fil dans lequel cette exception a été lancée? La réponse est concise et simple: non. Si une exception n'est pas interceptée sur l'ensemble des méthodes appelées, elle ne peut pas être gérée en principe. Sinon, une situation étrange se produit: si nous utilisons une exception AppDomain.FirstChanceException
gérer (une sorte de catch
synthétique), dans quel cadre la pile de threads doit-elle revenir? Comment définir cela dans le cadre des règles .NET CLR? Pas question. Ce n'est tout simplement pas possible. La seule chose que nous pouvons faire est d'enregistrer les informations reçues pour de futures recherches.
La deuxième chose à parler de «à terre» est la raison pour laquelle ces événements ont été présentés non pas à Thread
, mais à AppDomain
. Après tout, si vous suivez la logique, des exceptions surviennent où? Dans le flux d'exécution des commandes. C'est-à-dire en fait Thread
. Alors pourquoi le domaine a-t-il des problèmes? La réponse est très simple: dans quelles situations AppDomain.FirstChanceException
et AppDomain.UnhandledException
ont-ils été AppDomain.FirstChanceException
? Entre autres choses - pour créer des sandbox pour les plugins. C'est-à-dire pour les situations où un certain AppDomain
est configuré pour PartialTrust. Tout peut arriver à l'intérieur de cet AppDomain: des threads peuvent y être créés à tout moment, ou des threads existants de ThreadPool peuvent être utilisés. Ensuite, il s'avère que nous, étant en dehors de ce processus (nous n'avons pas écrit ce code), nous ne pouvons pas souscrire aux événements des flux internes. Tout simplement parce que nous n'avons aucune idée des flux qui y ont été créés. Mais nous sommes garantis d'avoir un AppDomain
qui organise le bac à sable et le lien vers lequel nous avons.
Donc, en fait, nous avons deux événements régionaux: quelque chose s'est passé qui n'était pas supposé ( FirstChanceExecption
) et "tout va mal", personne n'a géré l'exception: cela n'a pas été fourni. Par conséquent, le flux d'exécution des commandes n'a pas de sens et il ( Thread
) sera expédié.
Que peut-on obtenir en ayant ces événements et pourquoi est-il mauvais que les développeurs contournent ces événements?
AppDomain.FirstChanceException
Cet événement est par nature purement informatif et ne peut pas être «traité». Sa tâche est de vous informer qu'une exception s'est produite dans ce domaine et il commencera à être traité par le code d'application après le traitement de l'événement. Son exécution comporte quelques fonctionnalités dont il faut se souvenir lors de la conception du processeur.
Mais regardons d'abord un exemple synthétique simple de son traitement:
void Main() { var counter = 0; AppDomain.CurrentDomain.FirstChanceException += (_, args) => { Console.WriteLine(args.Exception.Message); if(++counter == 1) { throw new ArgumentOutOfRangeException(); } }; throw new Exception("Hello!"); }
Qu'est-ce qui est remarquable avec ce code? Partout où du code lève une exception, la première chose qui se produit est de le connecter à la console. C'est-à-dire même si vous oubliez ou ne pouvez pas envisager de gérer un type d'exception, il apparaîtra toujours dans le journal des événements que vous organisez. La seconde est une condition quelque peu étrange pour lever une exception interne. Le fait est qu'à l'intérieur du gestionnaire FirstChanceException
vous ne pouvez pas simplement lancer et FirstChanceException
une exception de plus. Au contraire, même ceci: à l'intérieur du gestionnaire FirstChanceException, vous n'êtes pas en mesure de lever au moins une exception. Si vous le faites, il y a deux événements possibles. Au début, s'il n'y avait pas de if(++counter == 1)
, nous obtiendrions une FirstChanceException
infinie pour une toute nouvelle ArgumentOutOfRangeException
. Qu'est-ce que cela signifie? Cela signifie qu'à un certain stade, nous aurions une StackOverflowException
: throw new Exception("Hello!")
FirstChanceException
méthode CLR Throw, qui lève FirstChanceException
, qui lève déjà Throw
pour ArgumentOutOfRangeException
, puis récursive. La deuxième option - nous nous sommes défendus par la profondeur de la récursivité en utilisant la counter
condition. C'est-à-dire dans ce cas, nous lançons une exception une seule fois. Le résultat est plus qu'inattendu: nous obtenons une exception qui fonctionne réellement à l'intérieur de l'instruction Throw
. Et qu'est-ce qui convient le mieux à ce type d'erreur? Selon ECMA-335, si une instruction a été lancée dans une exception, une ExecutionEngineException
doit être levée! Mais nous ne sommes pas en mesure de gérer cette situation exceptionnelle. Cela conduit à un crash complet de l'application. Quelles sont les options de traitement sécuritaires dont nous disposons?
La première chose qui me vient à l'esprit est de définir un bloc try-catch
sur tout le code du gestionnaire 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!
C'est-à-dire d'une part, nous avons le code pour gérer l'événement FirstChanceException
, et d'autre part, nous avons du code supplémentaire pour gérer les exceptions dans la FirstChanceException
elle-même. Cependant, les techniques de journalisation pour les deux situations doivent être différentes. Si la journalisation du traitement des événements peut se dérouler comme vous le souhaitez, le traitement des erreurs de logique de traitement FirstChanceException
doit aller sans exception en principe. La deuxième chose que vous avez probablement remarquée est la synchronisation entre les threads. Cela peut soulever la question: pourquoi est-il ici si une exception est levée dans un thread, ce qui signifie que FirstChanceException
doit être thread-safe. Cependant, tout n'est pas si gai. FirstChanceException
nous avons chez AppDomain. Et cela signifie qu'il se produit pour tout thread démarré dans un domaine particulier. C'est-à-dire si nous avons un domaine dans lequel plusieurs threads sont démarrés, FirstChanceException
peut aller en parallèle. Et cela signifie que nous devons en quelque sorte nous protéger avec la synchronisation: par exemple, en utilisant le lock
.
La deuxième façon consiste à essayer de détourner le traitement vers un thread voisin qui appartient à un domaine d'application différent. Cependant, il convient de mentionner qu'avec une telle implémentation, nous devons créer un domaine dédié spécifiquement pour cette tâche afin qu'il ne fonctionne pas afin que d'autres flux qui fonctionnent puissent mettre ce domaine:
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(); } } }
Dans ce cas, la gestion de FirstChanceException
est aussi sûre que possible: dans le thread voisin appartenant au domaine voisin. Dans ce cas, les erreurs de traitement d'un message ne peuvent pas provoquer de flux de travail d'application. De plus, vous pouvez écouter séparément l'exception UnhandledException du domaine de journalisation des messages: des erreurs fatales lors de la journalisation ne feront pas tomber l'application entière.
AppDomain.UnhandledException
Le deuxième message que nous pouvons attraper et qui traite de la gestion des exceptions est AppDomain.UnhandledException
. Ce message est une très mauvaise nouvelle pour nous car cela signifie qu'il n'y avait personne qui pouvait trouver un moyen de gérer l'erreur dans un certain thread. De plus, si une telle situation s'est produite, tout ce que nous pouvons faire est de "clarifier" les conséquences d'une telle erreur. C'est-à-dire de quelque manière que ce soit pour nettoyer les ressources appartenant uniquement à ce flux, le cas échéant. Cependant, une situation encore meilleure consiste à gérer les exceptions à la racine des threads sans bloquer le thread. C'est-à-dire mettre essentiellement try-catch
. Essayons de considérer la pertinence de ce comportement.
Supposons que nous ayons une bibliothèque qui doit créer des threads et implémenter une sorte de logique dans ces threads. En tant qu'utilisateurs de cette bibliothèque, nous souhaitons uniquement garantir les appels API et recevoir des messages d'erreur. Si la bibliothèque plante des flux sans en informer, cela ne peut pas nous aider beaucoup. De plus, l'effondrement du flux entraînera un message AppDomain.UnhandledException
, dans lequel il n'y a aucune information sur le flux particulier qui se trouve de son côté. Si nous parlons de notre code, il est peu probable qu'un flux en panne nous soit utile. En tout cas, je n'ai pas répondu à ce besoin. Notre tâche consiste à traiter correctement les erreurs, à envoyer des informations sur leur occurrence dans le journal des erreurs et à terminer correctement le flux. C'est-à-dire envelopper essentiellement la méthode avec laquelle le thread démarre dans 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"); } } });
Dans un tel schéma, nous obtenons ce dont nous avons besoin: d'une part, nous ne briserons pas le flux. D'autre part, nettoyez correctement les ressources locales si elles ont été créées. Eh bien, dans l'annexe - nous organisons l'enregistrement de l'erreur reçue. Mais attendez, dites-vous. D'une manière ou d'une autre, vous avez renommé le problème de l'événement AppDomain.UnhandledException
. N'est-ce vraiment pas nécessaire du tout? C'est nécessaire. Mais juste pour vous informer que nous avons oublié d'envelopper certains threads dans try-catch
avec toute la logique nécessaire. Avec tout: avec enregistrement et purification des ressources. Sinon, ce sera complètement faux: prenez et éteignez toutes les exceptions, comme si elles n'étaient pas là du tout.
Lien vers le livre entier
