[DotNetBook] Eventos de exceção e como obter StackOverflow e ExecutionEngineException do zero


Eventos de exceção


No caso geral, nem sempre sabemos sobre as exceções que ocorrerão em nossos programas porque quase sempre usamos algo que é escrito por outras pessoas e que está em outros subsistemas e bibliotecas. Não só pode haver uma variedade de situações em seu próprio código, no código de outras bibliotecas, como também existem muitos problemas associados à execução de código em domínios isolados. E apenas neste caso, seria extremamente útil poder receber dados sobre a operação de código isolado. Afinal, uma situação pode ser bastante real quando o código de terceiros intercepta todos os erros sem exceção, eliminando sua fault bloco:


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

Em tal situação, pode ser que a execução do código não seja mais tão segura quanto parece, mas não temos mensagens sobre problemas. A segunda opção é quando o aplicativo suprime alguma exceção, mesmo legal. E o resultado - a seguinte exceção em um local aleatório fará com que o aplicativo falhe no futuro devido a um erro aparentemente aleatório. Aqui eu gostaria de ter uma idéia de qual era o pano de fundo desse erro. Qual é o curso dos eventos que levaram a essa situação. E uma maneira de tornar isso possível é usar eventos adicionais relacionados a situações excepcionais: AppDomain.FirstChanceException e AppDomain.UnhandledException .


Nota


O capítulo publicado em Habré não é atualizado e, provavelmente, já está um pouco desatualizado. E, portanto, consulte o original para obter textos mais recentes:



De fato, quando você "lança uma exceção", o método usual de algum subsistema Throw interno é chamado, que por si só executa as seguintes operações:


  • Lança AppDomain.FirstChanceException
  • Procura por filtros correspondentes na cadeia de manipuladores
  • Faz com que o manipulador pré-role a pilha para o quadro desejado.
  • Se nenhum manipulador foi encontrado, um AppDomain.UnhandledException é AppDomain.UnhandledException , travando o encadeamento no qual a exceção ocorreu.

Deve-se fazer uma reserva imediatamente ao responder a uma pergunta que atormentou muitas mentes: é possível, de alguma forma, cancelar a exceção que ocorreu no código não controlado que é executado no domínio isolado sem, assim, interromper o segmento em que essa exceção foi lançada? A resposta é concisa e simples: não. Se uma exceção não for detectada em toda a gama de métodos chamados, ela não poderá ser tratada em princípio. Caso contrário, surge uma situação estranha: se usarmos uma AppDomain.FirstChanceException manipular (algum tipo de catch sintética) a exceção, em que quadro a pilha de threads deve reverter? Como definir isso como parte das regras do .NET CLR? De jeito nenhum. Simplesmente não é possível. A única coisa que podemos fazer é registrar as informações recebidas para pesquisas futuras.


A segunda coisa a falar sobre "em terra" é por que esses eventos foram introduzidos não no Thread , mas no AppDomain . Afinal, se você seguir a lógica, surgem exceções onde? No fluxo de execução de comandos. I.e. na verdade Thread . Então, por que o domínio tem problemas? A resposta é muito simples: para quais situações foram AppDomain.UnhandledException e AppDomain.UnhandledException ? Entre outras coisas - para criar caixas de areia para plugins. I.e. para situações em que existe um determinado AppDomain configurado para o PartialTrust. Tudo pode acontecer dentro deste AppDomain: os threads podem ser criados lá a qualquer momento ou os existentes no ThreadPool podem ser usados. Acontece que, estando fora deste processo (não escrevemos esse código), não podemos assinar os eventos de fluxos internos. Só porque não temos idéia de quais fluxos foram criados lá. Mas temos a garantia de ter um AppDomain que organize a sandbox e o link para o qual temos.


Portanto, de fato, recebemos dois eventos regionais: algo aconteceu que não era suposto ( FirstChanceExecption ) e "tudo está ruim", ninguém lidou com a exceção: não foi fornecido. Portanto, o fluxo de execução de comando não faz sentido e ( Thread ) será enviado.


O que pode ser obtido com esses eventos e por que é ruim que os desenvolvedores ignorem esses eventos?


AppDomain.FirstChanceException


Este evento é inerentemente puramente informativo por natureza e não pode ser "processado". Sua tarefa é notificá-lo de que ocorreu uma exceção nesse domínio e ela começará a ser processada pelo código do aplicativo após o processamento do evento. Sua execução possui alguns recursos que devem ser lembrados durante o design do processador.


Mas vamos primeiro ver um exemplo sintético simples de seu processamento:


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

O que é notável nesse código? Sempre que algum código lança uma exceção, a primeira coisa que acontece é registrá-lo no console. I.e. mesmo que você esqueça ou não consiga imaginar lidar com algum tipo de exceção, ele ainda aparecerá no log de eventos que você está organizando. A segunda é uma condição um tanto estranha para gerar uma exceção interna. O problema é que, dentro do manipulador FirstChanceException você não pode simplesmente lançar e lançar mais uma exceção. Em vez disso, mesmo isso: dentro do manipulador FirstChanceException, você não pode lançar pelo menos nenhuma exceção. Se você fizer isso, há dois eventos possíveis. Na primeira, se não houvesse uma if(++counter == 1) , FirstChanceException uma FirstChanceException infinita para uma nova ArgumentOutOfRangeException . O que isso significa? Isso significa que, em um certo estágio, StackOverflowException uma StackOverflowException : throw new Exception("Hello!") FirstChanceException método CLR Throw, que lança FirstChanceException , que lança Throw já para ArgumentOutOfRangeException e depois se repete. A segunda opção - nos defendemos pela profundidade da recursão usando a condição de counter . I.e. neste caso, lançamos uma exceção apenas uma vez. O resultado é mais do que inesperado: recebemos uma exceção que realmente funciona dentro da instrução Throw . E o que é mais adequado para esse tipo de erro? De acordo com o ECMA-335, se uma instrução foi lançada em uma exceção, uma ExecutionEngineException deve ser lançada! Mas não somos capazes de lidar com essa situação excepcional. Isso leva a uma falha completa do aplicativo. Quais são as opções de processamento seguro que temos?


A primeira coisa que vem à mente é definir um bloco try-catch em todo o código do manipulador 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! 

I.e. por um lado, temos o código para manipular o evento FirstChanceException e, por outro, temos código adicional para manipular exceções no próprio FirstChanceException . No entanto, as técnicas de log para as duas situações devem ser diferentes. Se o log de processamento de eventos puder ocorrer como você desejar, o processamento de erros lógicos de processamento FirstChanceException deverá ocorrer sem exceção em princípio. A segunda coisa que você provavelmente notou é a sincronização entre os threads. Isso pode levantar a questão: por que está aqui se alguma exceção é lançada em qualquer thread, o que significa que FirstChanceException deve ser seguro para threads. No entanto, nem tudo é tão alegre. FirstChanceException que temos no AppDomain. E isso significa que isso ocorre para qualquer thread iniciado em um domínio específico. I.e. se tivermos um domínio no qual vários threads forem iniciados, FirstChanceException poderá ser paralelo. E isso significa que precisamos nos proteger de alguma forma com a sincronização: por exemplo, usando lock .


A segunda maneira é tentar desviar o processamento para um thread vizinho que pertence a um domínio de aplicativo diferente. No entanto, vale ressaltar que, com essa implementação, devemos criar um domínio dedicado especificamente para esta tarefa, para que não funcione, para que outros fluxos que estejam funcionando possam colocar esse domínio:


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

Nesse caso, manipular FirstChanceException é o mais seguro possível: no thread vizinho pertencente ao domínio vizinho. Nesse caso, erros no processamento de uma mensagem não podem interromper os fluxos de trabalho do aplicativo. Além disso, você pode ouvir separadamente a UnhandledException do domínio de log de mensagens: erros fatais durante o log não derrubarão o aplicativo inteiro.


AppDomain.UnhandledException


A segunda mensagem que podemos capturar e que lida com o tratamento de exceções é AppDomain.UnhandledException . Essa mensagem é uma péssima notícia para nós, pois significa que não havia ninguém que pudesse encontrar uma maneira de lidar com o erro em um determinado segmento. Além disso, se tal situação ocorreu, tudo o que podemos fazer é "limpar" as consequências de tal erro. I.e. de qualquer forma, para limpar os recursos pertencentes apenas a esse fluxo, se houver algum. No entanto, uma situação ainda melhor é lidar com exceções enquanto estiver na raiz dos threads sem bloquear o thread. I.e. essencialmente colocar try-catch . Vamos tentar considerar a adequação desse comportamento.


Suponha que tenhamos uma biblioteca que precise criar threads e implementar algum tipo de lógica nesses threads. Nós, como usuários desta biblioteca, estamos interessados ​​apenas em garantir chamadas à API e em receber mensagens de erro. Se a biblioteca travar fluxos sem notificar sobre isso, isso não pode nos ajudar muito. Além disso, o colapso do fluxo levará a uma mensagem AppDomain.UnhandledException , na qual não há informações sobre qual fluxo específico fica do seu lado. Se estamos falando sobre nosso código, também é improvável que um fluxo com falha seja útil para nós. De qualquer forma, não atendi à necessidade disso. Nossa tarefa é processar os erros corretamente, enviar informações sobre a ocorrência para o log de erros e finalizar o fluxo corretamente. I.e. envolva essencialmente o método com o qual o thread inicia no 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"); } } }); 

Nesse esquema, obtemos o que precisamos: por um lado, não romperemos o fluxo. Por outro lado, limpe corretamente os recursos locais se eles foram criados. Bem, no apêndice - organizamos o registro do erro recebido. Mas espere, você diz. De alguma forma, você renunciou à edição do evento AppDomain.UnhandledException . Realmente não é necessário? É necessário. Mas apenas para informar que esquecemos de envolver alguns threads no try-catch com toda a lógica necessária. Com tudo: com registro e purificação de recursos. Caso contrário, será completamente errado: capturar e extinguir todas as exceções, como se elas não estivessem presentes.


Link para o livro inteiro



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


All Articles