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






4.76/5 (30投票s)
使用 SafeThread 防止辅助线程中未处理的异常
- 下载 SafeThread 演示 - 17.33 KB
- 下载 SafeThread 源代码 - 9.75 KB
- 下载 UnsafeThread 演示 - 6.37 KB
- 下载 UnsafeThread 源代码 - 9.3 KB

引言
几乎任何程序都可能发生未处理的异常。当它们发生在辅助(工作)线程中时,它们可能会导致应用程序崩溃——或者更糟,被忽略!使用 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();
除了 ThreadStart
和 ParameterizedThreadStart
,SafeThread
还提供了 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
来指示处理是否因异常而终止,以及有问题的 Exception
或 null
(在 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 线程Name
是SafeThread#XXX
,其中 XXX 是 CLRThread
对象的HashCode
。- 当与
ParameterizedThreadStart
一起使用时,SafeThread
会在其ThreadStartArg
属性中记住其启动参数。 SafeThread
提供了一个通用的Tag
对象属性,可用于记住有关线程的任意信息。SafeThread
提供了一个LastException
属性,该属性记录了SafeThread
捕获的最后一个异常。
注释
请注意,.NET 2.0 中辅助线程中未处理异常的行为发生了变化。在 .NET 1.1 中,工作线程 (ThreadPool
)(但不是 Timer
线程)中的未处理异常会被 AppDomain
的 UnhandledException
事件捕获。Microsoft 在应用程序配置文件中提供了一个向后兼容选项。
<runtime>
<legacyUnhandledExceptionPolicy enabled="1"/>
</runtime>
有关详细信息,请参阅 MSDN。
未来方向
使用 SafeThread
类的逻辑组件将是 SafeThreadPool
,SafeTimer
类也是如此。这些可能在未来的文章更新中涵盖。
修订历史
- 08-07-2008
- 文章初版发布
- 08-08-2008
- 添加了
UnsafeThread
演示和源代码,以展示没有SafeThread
会发生什么。 - 添加了
UnsafeThread
演示描述部分。 - 编辑了“背景”部分,以阐明不同类型辅助线程的不同行为。
- 编辑了“注意事项”部分,以阐明旧版兼容性选项的目的。
- 删除了额外的许可证部分。
- 添加了
- 08-17-2008
SafeThread
类已更新为发出ThreadCompleted
事件。SafeThreadDemo
应用程序已更新,以使用ParameterizedThreadStart
将生存时间参数传递给threadRunner
方法,并挂接ThreadCompleted
事件并在ListBox
中更新文本。- 已更正
SafeThreadDemo
应用程序,以从线程集合中删除已完成的SafeThread
。