【问题标题】:Why doesn't my process terminate when Task has unhandled exception?当 Task 有未处理的异常时,为什么我的进程没有终止?
【发布时间】:2017-03-02 17:53:58
【问题描述】:

我正在使用 .NET 4.0 构建 Windows 服务。

我在 Tasks 中抛出了各种未处理的异常,但它们并没有像 MSDN 文档所述那样终止我的进程(Parallel Tasks - 请参阅未观察到的任务异常)。

“如果你不给错误的任务传播它的机会 异常(例如,通过调用 Wait 方法),运行时将 根据当前升级任务的未观察到的异常 垃圾回收任务时的 .NET 异常策略。”

即使我使用最简单的任务调用,它的行为也是如此:

Task.Factory.StartNew(() => { throw new Exception(); } 

服务在被调用时继续正常运行。

根据文档,一旦任务被 GC 处理,任务的终结器将重新抛出异常,但这似乎不会发生。 MSDN 反复声明正常的“.NET 异常策略”会导致进程终止。

为什么这不会终止我的应用程序?我唯一能想到的就是以某种方式引用了某处的任务(是 lambda 吗??)

【问题讨论】:

  • 你强制GC了吗?终结器可能还没有运行。
  • 我将再次尝试测试 GC.Collect。想我上次是通过 ContinueWith 调用它...
  • 有趣的是,我正在查看 Task.cs 源代码(来自 MS 服务器和 Reflector),但我在代码中看不到终结器……我是盲目的,还是有其他方法可以完成它,还是文档错了?

标签: .net .net-4.0 task-parallel-library


【解决方案1】:

来自Essential C# 4.0,第 715 页,以下内容可能会对您有所帮助:

Task 执行期间未处理的异常将被抑制 直到调用任务完成成员之一:Wait(), Result,Task.WaitAll(),或Task.WaitAny()。每个 ot 这些成员将 抛出任务中发生的任何未处理的异常 执行。

有很多方法可以处理异常。有a look at MSDN here。

在回答您的评论时,同一本书的另一句话解释了为什么某些例外不传播:

虽然比较少见,但一般规则的例外之一 (冒泡)恰好在任务上。 [..] 任何基于任务的异常 在应用程序退出期间从终结队列中抛出将去 压制。行为是这样设置的,因为经常努力 处理这样的异常太复杂了[...]

要克服这个问题,一种优雅的方法是创建一个异常处理程序任务,并在任务运行后使用ContinueWith 进行跟进。然后,您可以使用parentTask.IsFaulted 并正常崩溃,即使在应用程序退出期间在终结队列中抛出异常的情况下也是如此。

提示:使用标志OnlyOnFaulted 让此任务仅在发生异常时运行。

【讨论】:

  • 是的,我知道。但我不想等待任务。根据文档,一旦完成,它应该抛出
  • 重点是,在任务的情况下,通常异常不会传播到父级,我的意思是,并非总是如此。
  • "如果您不给故障任务传播其异常的机会(例如,通过调用 Wait 方法),运行时将根据当前的 .NET 异常升级任务的未观察到的异常垃圾收集任务时的策略。”
  • 它也会调用 GC.WaitForPendingFinalizers。当然,问题在于 when 调用它。必须等待任务往往会破坏使用任务的意义。
  • 多年后的更新:我是 TPL 的常规用户,我处理任务(我关心的)“未处理”异常的方式是使用这个 ContinueWith/OnlyOnFaulted 组合。我触发了一个自定义的TaskTerminated 事件,如果我想终止我的进程,我会使用它。
【解决方案2】:

.NET 4.5 对how UnobservedExceptions are handled 做了一些更改

虽然未观察到的异常仍会导致 要引发 UnobservedTaskException 事件(不这样做将是 重大更改),默认情况下该进程不会崩溃。

但可以配置此行为,因此您可以通过启用 ThrowUnobservedTaskExceptions 来恢复到 .Net 4.0 行为,如下所示:

<configuration> 
    <runtime> 
        <ThrowUnobservedTaskExceptions enabled="true"/> 
    </runtime>
</configuration>

建议库开发人员在测试时启用此功能,以确保他们不会抛出任何 UnobservedExceptions。否则启用此设置的库使用者可能会看到他们的程序崩溃。

【讨论】:

  • 无论此设置如何,我的仍然在 .NET 4.6 中崩溃。调用 GC.Collect 后,没有 UnobservedTaskException 事件,进程立即退出,代码为零。
【解决方案3】:

正如@Hans 和@CodeInChaos 所建议的那样,我发现重新抛出未处理异常(从而终止进程)的唯一方法是强制终结器运行(注意:确保不要这样做在ContinueWith()!):

GC.Collect(); 
GC.WaitForPendingFinalizers();

在我的特定情况下,任务没有被垃圾收集,因为程序的流程取决于任务是否成功。如果流程不继续,我的应用程序将不会做任何导致 GC(分配对象等)的事情。

有趣的是,即使做一个GC.Collect() 也是不够的。任务终结器仍然没有运行。 GC.WaitForPendingFinalizers() 必须显式调用。 (我怀疑我不了解 Finalization 的微妙之处)。

总结一下:不要期望 TPL 任务的未观察到的异常 行为类似于其他线程机制未处理的异常 行为(例如QueueUserWorkItem)。在大多数实际情况下,您需要显式检查任务中的异常:您不能像使用 QUWI 或类似方式那样依赖未观察到的异常引起您的注意,因为您只会看到它们从 Finalizer 抛出,这是完全不可预测的。

编辑:查看我关于 .NET 4.5 的其他答案

【讨论】:

  • 任务中未观察到的异常通常表明存在错误。它们不是错误报告机制。因此不需要非常可靠的机制。
  • 所以现在我们有两个原因导致 Tasks 中没有出现异常:终结器没有运行,或者在应用程序关闭期间在终结队列中发生异常。
  • @CodeInChaos。您是否知道 .NET 2.0 中对异常策略所做的全面更改?他们使所有未处理的异常使进程崩溃,正是因为人们没有意识到异常的发生。换句话说,它们是“未被观察到的”。为什么 TPL 不应该遵循让用户知道发生了异常的框架的相同原则?当然,未观察到的异常表明存在错误。而且我想知道错误(默认情况下),而不是隐藏它们。
  • 我能够通过使用 ContinueWith 来完成这项工作,但随后在 UI 线程上调用。
【解决方案4】:

您可以使用TaskCreationOptions.AttachedToParent 创建它。根据Nested Tasks and Child Tasks (MSDN),异常会传播到你的线程。但是,我不知道这是否优雅。

Microsoft 不建议“在大多数情况下”这样做。其他人可能知道在什么情况下这可能是明智的。来自同一篇文章:

您可以使用附加的子任务来创建紧密同步的 异步操作图。但是,在大多数情况下,我们 建议您使用嵌套任务,因为与 其他任务不太复杂。这就是为什么在其他内部创建任务的原因 默认情况下,任务是嵌套的,您必须明确指定 AttachedToParent 选项来创建子任务。

干杯,马蒂亚斯

【讨论】:

  • 感谢您的帮助!
【解决方案5】:

根据这个漂亮的blog post,如果您想在您的任务中有未处理的异常时立即使您的应用程序崩溃,那么您可以继续您的任务,如下所示:

public static Task FailFastOnException(this Task task) 
{ 
    task.ContinueWith(c => Environment.FailFast(“Task faulted”, c.Exception), 
    TaskContinuationOptions.OnlyOnFaulted | 
    TaskContinuationOptions.ExecuteSynchronously | 
    TaskContinuationOptions.DetachedFromParent); 
    return task; 
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-02-20
    • 1970-01-01
    • 1970-01-01
    • 2018-10-15
    • 1970-01-01
    • 1970-01-01
    • 2018-07-01
    • 1970-01-01
    相关资源
    最近更新 更多