
异常事件
在一般情况下,我们并不总是知道程序中会发生的异常,因为几乎总是我们使用别人编写的东西以及其他子系统和库中的东西。 在您自己的代码中,以及在其他库的代码中,不仅可能存在多种情况,而且在隔离域中执行代码还存在许多问题。 在这种情况下,能够接收有关隔离代码操作的数据将非常有用。 毕竟,当第三方代码无例外地拦截所有错误, fault
块将其fault
淹没时,情况可能会很真实:
try { // ... } catch { // do nothing, just to make code call more safe }
在这种情况下,可能会发现代码执行不再像看起来那样安全,但是我们没有任何有关任何问题的消息。 第二种选择是当应用程序禁止某些甚至合法的异常时。 结果-随机位置的以下异常将导致应用程序在将来因看似随机的错误而崩溃。 在这里,我想知道此错误的背景是什么。 事件的发展过程导致了这种情况。 实现此目的的一种方法是使用与特殊情况有关的其他事件: AppDomain.FirstChanceException
和AppDomain.UnhandledException
。
注意事项
在哈布雷(Habré)上发表的这一章没有更新,可能有点过时了。 因此,请转到原始文本以获取更多最新文本:

实际上,当您“引发异常”时,将调用某些内部Throw
子系统的常用方法,该子系统内部将执行以下操作:
- 引发
AppDomain.FirstChanceException
- 在处理程序链中搜索匹配的过滤器
- 使处理程序将堆栈预滚动到所需的帧。
- 如果未找到任何处理程序,则会
AppDomain.UnhandledException
,从而使发生异常的线程崩溃。
人们在回答一个困扰很多人的问题时应该立即保留一个意见:是否有可能以某种方式取消在隔离域中执行的不受控制的代码中发生的异常,而不会破坏抛出该异常的线程? 答案简洁明了:不。 如果没有在调用的全部方法上捕获到异常,则原则上无法处理该异常。 否则,就会出现一个奇怪的情况:如果我们使用AppDomain.FirstChanceException
处理(某种合成catch
)异常,那么线程堆栈应该回滚到哪个帧? 如何将其设置为.NET CLR规则的一部分? 没办法 根本不可能。 我们唯一能做的就是记录收到的信息以备将来研究。
关于“上岸”的第二件事是为什么这些事件不是在Thread
而是AppDomain
引入的。 毕竟,如果您遵循逻辑,异常会在哪里出现? 在命令执行流程中。 即 实际上是Thread
。 那么,域为何有问题? 答案很简单:在什么情况下AppDomain.UnhandledException
AppDomain.FirstChanceException
和AppDomain.UnhandledException
? 除其他外-为插件创建沙箱。 即 对于某些为PartialTrust配置的AppDomain
的情况。 这个AppDomain内部可能发生任何事情:可以随时在其中创建线程,也可以使用ThreadPool中的现有线程。 事实证明,在此流程之外(我们没有编写该代码),我们无法订阅内部流程的事件。 只是因为我们不知道在那里创建了什么流程。 但是我们保证有一个AppDomain
可以组织沙箱和我们拥有的链接。
因此,实际上,我们为我们提供了两个区域性事件:一件不应该发生的事情( 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)
条件,我们将为全新的ArgumentOutOfRangeException
获得无限的FirstChanceException
。 这是什么意思? 这意味着在某个阶段,我们将获得StackOverflowException
: throw new Exception("Hello!")
FirstChanceException
CLR Throw方法,该方法抛出FirstChanceException
,该方法已经为ArgumentOutOfRangeException
抛出了Throw
,然后递归。 第二种选择-我们使用counter
条件通过递归的深度为自己辩护。 即 在这种情况下,我们只会抛出一次异常。 结果超出了预料之外:我们得到了一个在Throw
指令中实际起作用的异常。 哪种错误最适合此类错误? 根据ECMA-335,如果将指令引发异常,则必须引发ExecutionEngineException
! 但是我们无法处理这种特殊情况。 这会导致应用程序彻底崩溃。 我们有哪些安全处理选项?
首先想到的是在FirstChanceException
处理程序的整个代码上设置一个try-catch
块:
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
应该是线程安全的。 但是,一切并不那么愉快。 我们在AppDomain中拥有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
尽可能安全:在属于相邻域的相邻线程中。 在这种情况下,处理消息时的错误不会降低应用程序的工作流程。 另外,您可以单独侦听消息记录域的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
。 拥有一切:拥有日志记录和净化资源。 否则,那将是完全错误的:采取并扑灭所有异常,就好像它们根本不存在一样。
链接到整本书
