65.9K
CodeProject 正在变化。 阅读更多。
Home

SafeThread - 防止辅助线程中的未处理异常

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (30投票s)

2008年8月7日

CPOL

6分钟阅读

viewsIcon

103947

downloadIcon

2333

使用 SafeThread 防止辅助线程中未处理的异常

引言

几乎任何程序都可能发生未处理的异常。当它们发生在辅助(工作)线程中时,它们可能会导致应用程序崩溃——或者更糟,被忽略!使用 SafeThread 来防止辅助线程中的未处理异常。

目录

背景

在为 CALM 项目 研究 .NET 2.0 应用程序中未处理异常的行为时,最令人惊讶和沮丧的发现之一是辅助线程中的未处理异常如何倾向于导致应用程序崩溃或完全被忽略!辅助线程(您显式创建的线程)中的未处理异常会使应用程序崩溃,即使您有未处理的异常事件处理程序(而且上下文信息,即哪个线程,也会丢失)。特别是,公共语言运行时 (CLR) 会忽略工作线程 (ThreadPool) 和 Timer 线程中的未处理异常。这些线程会悄无声息地终止,没有未处理异常事件可以捕获,没有给用户任何警告,什么都没有。显然,在生产应用程序中,这是不可接受的(错误的)行为!

SafeThread

因此,创建了 SafeThread 类。SafeThread 包装了一个常规的 CLR Thread,但在一个 try-catch 块中执行方法委托,该块在发生未处理异常时发出事件。开发人员可以使用此事件来清理线程,对异常进行分类,甚至启动新线程。对于某些类型的线程操作,例如“心跳”操作,这对于健壮的应用程序来说是宝贵且必需的。

SafeThread 背后的原理相当简单。由于线程依赖方法委托进行执行,因此我们只需要将委托的执行包装在一个 try-catch 块中,并在捕获到异常时发出一个事件。基类 Thread 支持单个参数委托和无参数委托,因此 SafeThread 模仿了这些构造函数。此外,SafeThread 支持新的动态委托(也称为匿名方法),以提供额外的便利。最后,为了完成外观,SafeThread 实现​​了 Thread 类所有 public 方法和属性。SafeThread 还提供了一个 ThreadCompleted 事件来指示处理何时完成。

SafeThread 实现

显然,最好 SafeThread 能够继承自 Thread,但这不可能,因为 Thread 类是 sealed(在 VB.NET 术语中是 MustInherit)。次优的解决方案是让 SafeThread 实现​​一个通用的线程接口(例如 IThread),但遗憾的是,.NET 也没有这个。因此,我们所能做的最好的就是重新实现接口并包装一个 Thread 对象。请注意,这会带来一些问题,因为 SafeThread 的某些属性和方法只有在已包装的 Thread 存在时才有效,因为已包装的 Thread 对象直到调用 Start 方法后才创建。

SafeThread 用法

SafeThread 可以像 CLR Thread 类一样使用,例如,使用 ThreadStart,并增加了 ThreadException 事件和一个标志来控制调用线程的 Abort 是否被报告为异常。

SafeThread thrd = new SafeThread(new ThreadStart(this.threadRunner));
thrd.ShouldReportThreadAbort = true;
thrd.ThreadException += 
     new ThreadThrewExceptionHandler(thrd_ThreadException);
thrd.ThreadCompleted += 
     new ThreadCompletedHandler(thrd_ThreadCompleted);
thrd.Start();

除了 ThreadStartParameterizedThreadStartSafeThread 还提供了 SimpleDelegate 选项,可用于任何 void 无参数方法。

SafeThread thrd = new SafeThread((SimpleDelegate)this.threadRunner));
thrd.ShouldReportThreadAbort = true;
thrd.ThreadException += 
     new ThreadThrewExceptionHandler(thrd_ThreadException);
thrd.ThreadCompleted += 
     new ThreadCompletedHandler(thrd_ThreadCompleted);
thrd.Start();

SimpleDelegate 也可用于匿名方法。

SafeThread thrd = new SafeThread((SimpleDelegate) 
           delegate { this.threadRunner(); });
thrd.ShouldReportThreadAbort = true;
thrd.ThreadException += 
     new ThreadThrewExceptionHandler(thrd_ThreadException);
thrd.ThreadCompleted += 
     new ThreadCompletedHandler(thrd_ThreadCompleted);
thrd.Start();

ThreadException 处理程序会接收到抛出异常的 SafeThread 对象,以及抛出的 Exception

void thrd_ThreadException(SafeThread thrd, Exception ex)
{
    //do something here, like restart the thread 
}

ThreadComleted 处理程序会接收到完成处理的 SafeThread 对象,以及一个 bool 来指示处理是否因异常而终止,以及有问题的 Exceptionnull(在 VB.NET 术语中为 Nothing)如果处理成功完成。

void thrd_ThreadCompleted(SafeThread thrd, bool hadException, Exception ex)
{
    if (hadException)
    {
        //the thread terminated early due to an unhandled exception in ex
    }
    else
    {
        //the thread completed successfully
    }
}

SafeThread 演示应用程序

SafeThread 演示应用程序既做作又愚蠢:每个创建的 SafeThread 会周期性地使“开始 SafeThread”按钮的动画呈圆形脉动。每个创建的 SafeThread 会 99% 的时间脉动动画,1% 的时间抛出一个未处理的除零异常。如果 SafeThread 成功存活 100 次迭代,它就会成功完成并发出 ThreadCompleted 事件。ListBox 显示了每个 SafeThread 的情况。

要使用 SafeThread 演示应用程序,请构建和/或运行它。多次单击“开始 SafeThread”按钮——创建的 SafeThread 对象越多,按钮在圆形中移动的速度就越快。单击“停止 SafeThread”按钮以按创建顺序结束 SafeThread 对象。单击 ListBox 项目以在 MessageBox 中查看完整的项目文本。所有 SafeThread 对象将在应用程序关闭时结束。

UnsafeThread 演示应用程序

为了好玩,并回应 Daniel Grunwald 在下面的评论中提出的几点,我在文章中添加了一个“不安全”的线程演示应用程序,以更好地说明在没有异常捕获或 SafeThread 的情况下会发生什么。单击“开始不安全线程”按钮可以启动一个常规的 CLR Thread,该线程会抛出一个未处理的除零异常,并注意,在没有任何保护的情况下,您会看到“发送调试报告给 Microsoft”对话框,应用程序崩溃。再次运行演示,但这次,首先选中“使用未处理异常处理程序”复选框,然后单击“开始不安全线程”按钮。现在,我们收到一个消息框,这是由 AppDomain.UnhandledException 事件引起的……然后,我们看到“发送调试报告给 Microsoft”对话框,应用程序崩溃。

这说明了仅使用 System.AppDomain.CurrentDomain.UnhandledException 事件处理程序来捕获“纯粹”辅助线程(即非工作线程或 Timer 线程)中的异常所带来的几个问题:

  • 事件处理程序中没有足够的上下文信息来知道是哪个线程导致了问题;sender 参数是应用程序,而不是线程。
  • 到未处理异常到达事件处理程序时,已经太晚了,无法采取任何措施——应用程序已在终止!
  • 无论您做什么,应用程序都会崩溃。
  • 最后——这是我最不喜欢此机制的一点——在您的未处理异常事件处理程序完成后,会出现可怕的“发送调试报告给 Microsoft”对话框!

结论

SafeThread 还有一些其他值得关注的属性。

  • SafeThread 有一个 Name 属性,该属性在 Start 时传递给底层 CLR 线程。默认的 CLR 线程 NameSafeThread#XXX,其中 XXX 是 CLR Thread 对象的 HashCode
  • 当与 ParameterizedThreadStart 一起使用时,SafeThread 会在其 ThreadStartArg 属性中记住其启动参数。
  • SafeThread 提供了一个通用的 Tag 对象属性,可用于记住有关线程的任意信息。
  • SafeThread 提供了一个 LastException 属性,该属性记录了 SafeThread 捕获的最后一个异常。

注释

请注意,.NET 2.0 中辅助线程中未处理异常的行为发生了变化。在 .NET 1.1 中,工作线程 (ThreadPool)(但不是 Timer 线程)中的未处理异常会被 AppDomainUnhandledException 事件捕获。Microsoft 在应用程序配置文件中提供了一个向后兼容选项。

<runtime>
    <legacyUnhandledExceptionPolicy enabled=&quot;1&quot;/>
</runtime>

有关详细信息,请参阅 MSDN

未来方向

使用 SafeThread 类的逻辑组件将是 SafeThreadPoolSafeTimer 类也是如此。这些可能在未来的文章更新中涵盖。

修订历史

  • 08-07-2008
    • 文章初版发布
  • 08-08-2008
    • 添加了 UnsafeThread 演示和源代码,以展示没有 SafeThread 会发生什么。
    • 添加了 UnsafeThread 演示描述部分。
    • 编辑了“背景”部分,以阐明不同类型辅助线程的不同行为。
    • 编辑了“注意事项”部分,以阐明旧版兼容性选项的目的。
    • 删除了额外的许可证部分。
  • 08-17-2008
    • SafeThread 类已更新为发出 ThreadCompleted 事件。
    • SafeThreadDemo 应用程序已更新,以使用 ParameterizedThreadStart 将生存时间参数传递给 threadRunner 方法,并挂接 ThreadCompleted 事件并在 ListBox 中更新文本。
    • 已更正 SafeThreadDemo 应用程序,以从线程集合中删除已完成的 SafeThread
© . All rights reserved.