[DotNetBook] Eventos de excepción y cómo obtener StackOverflow y ExecutionEngineException desde cero


Eventos de excepción


En el caso general, no siempre conocemos las excepciones que ocurrirán en nuestros programas porque casi siempre usamos algo escrito por otras personas y que está en otros subsistemas y bibliotecas. No solo puede haber una variedad de situaciones en su propio código, en el código de otras bibliotecas, también hay muchos problemas asociados con la ejecución de código en dominios aislados. Y solo en este caso sería extremadamente útil poder recibir datos sobre la operación de código aislado. Después de todo, una situación puede ser bastante real cuando el código de un tercero intercepta todos los errores sin excepción, ahogando su fault bloqueo:


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

En tal situación, puede resultar que la ejecución del código ya no sea tan segura como parece, pero no tenemos ningún mensaje sobre ningún problema. La segunda opción es cuando la aplicación suprime alguna excepción, incluso legal. Y el resultado: la siguiente excepción en un lugar aleatorio hará que la aplicación se bloquee en un futuro debido a un error aparentemente aleatorio. Aquí me gustaría tener una idea de los antecedentes de este error. ¿Cuál es el curso de los acontecimientos condujo a esta situación? Y una forma de hacer esto posible es usar eventos adicionales relacionados con situaciones excepcionales: AppDomain.FirstChanceException y AppDomain.UnhandledException .


Nota


El capítulo publicado en Habré no está actualizado y, probablemente, está un poco desactualizado. Y, por lo tanto, consulte el original para obtener un texto más reciente:



De hecho, cuando "lanza una excepción", se llama al método habitual de algún subsistema Throw interno, que en su interior realiza las siguientes operaciones:


  • Lanza AppDomain.FirstChanceException
  • Busca filtros coincidentes en la cadena de controladores
  • Hace que el controlador pre-ruede la pila al marco deseado.
  • Si no se encuentra ningún controlador, se AppDomain.UnhandledException una AppDomain.UnhandledException , que bloquea el hilo en el que se produjo la excepción.

Uno debe hacer una reserva inmediatamente al responder una pregunta que atormentó a muchas mentes: ¿es posible cancelar de alguna manera la excepción que ocurrió en el código no controlado que se ejecuta en el dominio aislado sin romper el hilo en el que se lanzó esta excepción? La respuesta es concisa y simple: no. Si no se detecta una excepción en toda la gama de métodos llamados, no se puede manejar en principio. De lo contrario, surge una situación extraña: si usamos un AppDomain.FirstChanceException manejar (algún tipo de catch sintética) excepción, entonces ¿a qué marco debería volver la pila de hilos? ¿Cómo configurar esto como parte de las reglas de .NET CLR? De ninguna manera Simplemente no es posible. Lo único que podemos hacer es registrar la información recibida para futuras investigaciones.


La segunda cosa para hablar sobre "en tierra" es por qué estos eventos se introdujeron no en Thread , sino en AppDomain . Después de todo, si sigue la lógica, ¿dónde surgen excepciones? En el flujo de ejecución del comando. Es decir En realidad Thread . Entonces, ¿por qué el dominio tiene problemas? La respuesta es muy simple: ¿para qué situaciones se AppDomain.UnhandledException AppDomain.FirstChanceException y AppDomain.UnhandledException ? Entre otras cosas, para crear cajas de arena para complementos. Es decir para situaciones en las que hay un determinado AppDomain configurado para PartialTrust. Cualquier cosa puede suceder dentro de este AppDomain: los subprocesos se pueden crear allí en cualquier momento, o se pueden usar los existentes de ThreadPool. Entonces resulta que nosotros, estando fuera de este proceso (no escribimos ese código), no podemos suscribirnos a los eventos de flujos internos. Simplemente porque no tenemos idea de qué flujos se crearon allí. Pero tenemos la garantía de tener un AppDomain que organiza el sandbox y el enlace al que tenemos.


Entonces, de hecho, contamos con dos eventos regionales: sucedió algo que no se suponía ( FirstChanceExecption ) y "todo está mal", nadie manejó la excepción: no se proporcionó. Por lo tanto, el flujo de ejecución del comando no tiene sentido y se enviará ( Thread ).


¿Qué se puede obtener al tener estos eventos y por qué es malo que los desarrolladores eviten estos eventos?


AppDomain.FirstChanceException


Este evento es de naturaleza puramente informativa y no puede ser "procesado". Su tarea es notificarle que se ha producido una excepción dentro de este dominio y que el código de la aplicación comenzará a procesarlo después de procesar el evento. Su ejecución conlleva un par de características que deben recordarse durante el diseño del procesador.


Pero primero veamos un ejemplo sintético simple de su procesamiento:


 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é tiene de notable este código? Donde sea que un código arroje una excepción, lo primero que sucede es registrarlo en la consola. Es decir incluso si olvida o no puede imaginar el manejo de algún tipo de excepción, seguirá apareciendo en el registro de eventos que está organizando. El segundo es una condición algo extraña para lanzar una excepción interna. La FirstChanceException es que dentro del controlador FirstChanceException no puede lanzar y lanzar una excepción más. Más bien, incluso esto: dentro del controlador FirstChanceException, no puede lanzar al menos ninguna excepción. Si lo hace, hay dos posibles eventos. En el primero, si no if(++counter == 1) condición if(++counter == 1) , obtendríamos una FirstChanceException infinita para una ArgumentOutOfRangeException nueva. ¿Qué significa esto? Esto significa que en cierta etapa obtendríamos una StackOverflowException : throw new Exception("Hello!") FirstChanceException método CLR Throw, que arroja FirstChanceException , que arroja Throw ya para ArgumentOutOfRangeException y luego vuelve a aparecer. La segunda opción: nos defendimos por la profundidad de la recursión utilizando la condición de counter . Es decir en este caso, lanzamos una excepción solo una vez. El resultado es más que inesperado: obtenemos una excepción que realmente funciona dentro de la instrucción Throw . ¿Y qué es lo más adecuado para este tipo de error? De acuerdo con ECMA-335, si se arrojó una instrucción a una excepción, se debe lanzar una ExecutionEngineException . Pero no podemos manejar esta situación excepcional. Conduce a un bloqueo completo de la aplicación. ¿Cuáles son las opciones de procesamiento seguro que tenemos?


Lo primero que viene a la mente es establecer un bloque try-catch en todo el código del controlador 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! 

Es decir por un lado, tenemos el código para manejar el evento FirstChanceException , y por otro, tenemos código adicional para manejar excepciones en el propio FirstChanceException . Sin embargo, las técnicas de registro para ambas situaciones deberían ser diferentes. Si el registro de procesamiento de eventos puede ir a su gusto, entonces el procesamiento de errores de lógica de procesamiento de FirstChanceException debería ir sin excepción en principio. La segunda cosa que probablemente notó es la sincronización entre hilos. Esto puede plantear la pregunta: ¿por qué está aquí si se produce una excepción en algún subproceso, lo que significa que FirstChanceException debe ser seguro para subprocesos. Sin embargo, no todo es tan alegre. FirstChanceException que tenemos en AppDomain. Y esto significa que ocurre para cualquier hilo iniciado en un dominio particular. Es decir Si tenemos un dominio dentro del cual se inician varios subprocesos, FirstChanceException puede ir en paralelo. Y esto significa que necesitamos protegernos de alguna manera con la sincronización: por ejemplo, usando el lock .


La segunda forma es intentar desviar el procesamiento a un subproceso vecino que pertenece a un dominio de aplicación diferente. Sin embargo, vale la pena mencionar que con tal implementación, debemos construir un dominio dedicado específicamente para esta tarea para que no funcione y que otros flujos que estén funcionando puedan colocar este dominio:


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

En este caso, manejar FirstChanceException es lo más seguro posible: en el hilo vecino que pertenece al dominio vecino. En este caso, los errores al procesar un mensaje no pueden reducir los flujos de trabajo de la aplicación. Además, puede escuchar por separado la excepción UnhandledException del dominio de registro de mensajes: los errores fatales durante el registro no derribarán toda la aplicación.


AppDomain.UnhandledException


El segundo mensaje que podemos atrapar y que trata con el manejo de excepciones es AppDomain.UnhandledException . Este mensaje es una muy mala noticia para nosotros porque significa que no había nadie que pudiera encontrar la manera de manejar el error en un determinado hilo. Además, si tal situación ha ocurrido, todo lo que podemos hacer es "aclarar" las consecuencias de tal error. Es decir de ninguna manera para limpiar los recursos que pertenecen solo a esta secuencia si se creó alguno. Sin embargo, una situación aún mejor es manejar excepciones mientras está en la raíz de los hilos sin bloquear el hilo. Es decir esencialmente poner try-catch . Tratemos de considerar la idoneidad de este comportamiento.


Supongamos que tenemos una biblioteca que necesita crear hilos e implementar algún tipo de lógica en estos hilos. Nosotros, como usuarios de esta biblioteca, solo estamos interesados ​​en garantizar llamadas API y en recibir mensajes de error. Si la biblioteca bloquea las transmisiones sin notificarlo, esto no nos puede ayudar mucho. Además, el colapso de la secuencia dará lugar a un mensaje AppDomain.UnhandledException , en el que no hay información sobre qué secuencia particular se encuentra de su lado. Si estamos hablando de nuestro código, es poco probable que una secuencia que se cuelgue sea útil para nosotros. En cualquier caso, no satisfacía la necesidad de esto. Nuestra tarea es procesar los errores correctamente, enviar información sobre su aparición al registro de errores y finalizar correctamente el flujo. Es decir esencialmente envolver el método con el que el hilo comienza en 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"); } } }); 

En tal esquema, obtenemos lo que necesitamos: por un lado, no vamos a romper la corriente. Por otro lado, limpie correctamente los recursos locales si se crearon. Bueno, en el apéndice, organizamos el registro del error recibido. Pero espera, dices. De alguna manera, rebotó el tema del evento AppDomain.UnhandledException . ¿Realmente no es necesario en absoluto? Es necesario Pero solo para informar que olvidamos envolver algunos hilos en try-catch con toda la lógica necesaria. Con todo: con registro y purificación de recursos. De lo contrario, será completamente incorrecto: tomar y extinguir todas las excepciones, como si no estuvieran allí.


Enlace a todo el libro



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


All Articles