如何捕获/观察从任务中抛出的未处理异常

21

我正在尝试记录/报告我的应用程序中的所有未处理异常(错误报告解决方案)。我遇到了一种总是未处理的情况。我想知道如何以未处理的方式捕获此错误。请注意,今天早上我已经进行了大量研究并尝试了很多方法... 是的,我看过这个这个和许多其他东西。我只是在寻找通用解决方案以记录未处理的异常。

我在控制台测试应用程序的主要方法中有以下代码:

Task.Factory.StartNew(TryExecute);
或者
Task.Run((Action)TryExecute);

以及以下方法:

private static void TryExecute() {
   throw new Exception("I'm never caught");
}

我已经尝试将以下内容连接到我的应用程序中,但它们从未被调用。

AppDomain.CurrentDomain.UnhandledException
TaskScheduler.UnobservedTaskException

在我的Wpf应用程序中,我最初发现了这个错误,但我也连接了这些事件,但从未被调用。

Dispatcher.UnhandledException
Application.Current.DispatcherUnhandledException
System.Windows.Forms.Application.ThreadException

唯一被调用的处理程序是:

AppDomain.CurrentDomain.FirstChanceException

但这并不是一个有效的解决方案,因为我只想报告未捕获的异常(而不是每个异常,因为FirstChanceException会在任何catch块被执行/解决之前被调用)。


有趣的是,我并没有在任务中做过太多事情,但对于应用程序中未处理的异常,AppDomain UnhandledException 总是被调用。也许 Task API 自动捕获在任务中抛出的所有异常,并以某种方式重新路由它们。 - Mike Dinescu
我看到如果等待任务,它会触发UnhandledException。但是我不能保证开发人员在使用此报告库时会这样做。 - Blake Niemyjski
2个回答

26
TaskScheduler.UnobservedTaskException 事件应该能够满足您上述所述的需求。您认为它未触发的原因是什么? 特定情况下,任务会捕获异常并重新抛出(但不是立即)。任务中的异常以几种方式重新抛出(从我头脑中想到的,可能还有更多)。 1. 当您尝试访问结果(Task.Result)时 2. 在任务上调用 Wait(), Task.WaitOne(), Task.WaitAll() 或另一个相关的 Wait 方法时。 3. 没有显式查看或处理异常就尝试释放任务。
如果您执行了上述任何操作,则异常将在运行该代码的任何线程上重新抛出,并且由于您正在观察异常,因此不会调用事件。 如果您没有将代码放在 try {} catch {} 内部,则将触发 AppDomain.CurrentDomain.UnhandledException,这听起来可能是正在发生的事情。
异常重新抛出的另一种方式是:当您未执行上述任何操作时,任务仍将异常视为未被观察到,并且任务正在进行终结。这是最后的努力,以让您知道存在一个您没有看到的异常。
如果是这种情况,并且由于终结器是非确定性的,那么您是否正在等待进行垃圾回收以使具有未观察到异常的任务被放入终结器队列中,并再次等待它们被终结?

编辑:这篇文章谈到了一些关于此的内容。这篇文章则讲述了为什么存在该事件,这可能会让你了解如何正确使用它。


谢谢,这听起来就是发生的事情。我在处理程序中设置了断点,但它从未触发。如果我使用Wait(),我会在UnhandledException处理程序中捕获异常。我假设由于我没有(正在调试提交给我们的示例),GC尚未运行,这就是为什么我从未捕获它的原因。感谢您详细的回复,这对我帮助很大。 - Blake Niemyjski
第二篇文章似乎是这篇:https://devblogs.microsoft.com/pfxteam/faq-taskscheduler-unobservedtaskexception-event-doesnt-work/ 不过我找不到第一篇文章... - vividos
1
@vividos 谢谢。我找到了原始文章并将它们都更新了。你给的链接是第一篇文章。第二篇是由Stephen Toub撰写的“任务和未处理异常”。 - Christopher Currens

2
我使用了MSDN上的LimitedTaskScheduler来捕获所有异常,包括使用TPL的其他线程抛出的异常:
这是一个有限并发级别任务调度器的代码实现,继承自TaskScheduler类。其中包含了以下成员:
1. currentThreadIsProcessingItems:标识当前线程是否正在处理工作项。
2. tasks:待执行任务列表,使用LinkedList实现,由锁来保护。
3. logger:日志记录器。
4. maxDegreeOfParallelism:该调度器允许的最大并发级别。
5. delegatesQueuedOrRunning:当前排队或正在运行的委托数量,由锁来保护。
6. MaximumConcurrencyLevel:获取此调度程序支持的最大并发级别。
7. QueueTask:将任务加入到待执行任务列表中,并尝试在委托数量未达到最大并发级别时通知线程池执行任务。
8. TryExecuteTaskInline:尝试在当前线程上执行指定的任务。
9. TryDequeue:尝试从调度器中移除一个预定的任务。
10. GetScheduledTasks:获取当前在此调度器上预定的任务的可枚举对象。
11. OnTaskFault:当任务出现异常时调用。
12. NotifyThreadPoolOfPendingWork:通知线程池有待执行任务。
13. ExcuteTask:执行任务的方法,在此方法中循环处理待执行任务列表中的任务。

然后使用反射将TaskScheduler“注册”为默认值:


public static class TaskLogging
{
    private const BindingFlags StaticBinding = BindingFlags.Static | BindingFlags.NonPublic;
public static void SetScheduler(TaskScheduler taskScheduler) { var field = typeof(TaskScheduler).GetField("s_defaultTaskScheduler", StaticBinding); field.SetValue(null, taskScheduler);
SetOnTaskFactory(new TaskFactory(taskScheduler)); }
private static void SetOnTaskFactory(TaskFactory taskFactory) { var field = typeof(Task).GetField("s_factory", StaticBinding); field.SetValue(null, taskFactory); } }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接