【问题标题】:system.threading.task - why does TaskScheduler.UnobservedTaskException event not occur? Can I fix this?system.threading.task - 为什么不发生 TaskScheduler.UnobservedTaskException 事件?我可以解决这个问题吗?
【发布时间】:2014-05-08 07:10:58
【问题描述】:

我见过的一个常见问题是在任务中管理未处理的异常。它们不会导致崩溃,它们会默默地发生,当任务失败时我什至无法触发事件!我见过用户开出自定义类和东西来处理这个问题,我的问题是,有没有“标准”的微软方法来处理这个问题?

为了举例,我制作了这个简单的控制台应用程序 (.NET 4.5.1) 来演示这个问题。是否可以对其进行修改,以便这些任务可以异步执行,但在遇到未处理的异常时调用“处理程序”?或者至少崩溃?我认为这正是 UnobservedTaskException 应该做的。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApplication4
{
class Program
{
    static void Main(string[] args)
    {
        TaskScheduler.UnobservedTaskException += Handler;
        AppDomain.CurrentDomain.UnhandledException += Handler;
        Task.Run(() => { throw new ApplicationException("I'll throw an unhandled exception"); });
        Task.Factory.StartNew(() => { throw new ApplicationException("I'll throw an unhandled exception too"); });
        System.Threading.Thread.Sleep(2000);
        Console.WriteLine("I think everything is just peachy!");
        System.Threading.Thread.Sleep(10000);
    }
    private static void Handler(Object sender, EventArgs e)
    {
        Console.WriteLine("I'm so lonely, won't anyone call me?");
    }
}
}

输出:

I think everything is just peachy!

期望的输出:

I'm so lonely, won't anyone call me?
I'm so lonely, won't anyone call me?
I think everything is just peachy!

和/或简单地崩溃,即使这将是对异步任务静默失败的巨大改进!

编辑:根据这篇 MSDN 文章 http://msdn.microsoft.com/en-us/library/jj160346%28v=vs.110%29.aspx 将此添加到 app.config,但没有更改:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <runtime>
    <ThrowUnobservedTaskExceptions enabled="true"/>
  </runtime>
</configuration>

【问题讨论】:

  • 关于您的编辑,Reed 的答案和我上面链接的答案都解释了为什么没有变化:任务需要获得 GC'ed。这背后有充分的理由,请阅读链接了解更多详情。
  • 我可以看出一切都是有原因的,但我不能否认有充分的理由不应该按设计发生的想法让我大吃一惊。如果我不想等待任务怎么办,在这种情况下我是否应该永远不要使用任务类而使用其他类呢?如果我确实需要等待,那么拥有一个可以订阅的事件有什么缺点?我要写一个扩展方法,一定要告诉我为什么不应该这样……
  • 我已回复您的评论here

标签: c# task-parallel-library


【解决方案1】:

来自评论:

我可以看到一切都是有原因的,但我不能否认 对有充分理由的想法感到震惊 不应按设计发生。如果我不想等待任务怎么办, 在这种情况下,我应该永远不要使用 Task 类吗?是什么 有一个可以订阅的事件的缺点?我要去 写一个扩展方法,无论如何,告诉我为什么 不应该这样

Task 对象的本质是它会在未来完成。任务的结果或异常也有望在未来得到观察,很可能在不同的堆栈帧上,甚至在不同的线程上。这就是为什么框架(或编译器生成的代码,用于async Task 方法)将异常存储在任务中并且不会立即抛出它。

您不会在此处观察任务的结果,甚至不会在任何地方存储对任务对象的引用。从本质上讲,您正在执行一个即发即弃的呼叫。编译器会对此发出警告(告诉任务没有等待),但它并没有消除托管Task 对象仍然被创建并一直挂起直到它被垃圾收集的事实。此时,TaskScheduler.UnobservedTaskException 事件将被触发。

...有充分的理由不应该按设计发生

现在,如果上述方法是一个糟糕的设计,那么什么是好的设计呢?如果会立即抛出异常,那么以下可能如何工作:

var task = Task.Run(() => {
    throw new ApplicationException("I'll throw an unhanded exception"); });
Thread.Sleep(1000);
if (task.IsFaulted)
  Console.WriteLine(task.Exception.InnerException.Message);

它根本做不到。 Sleep 之后的代码将没有机会按照自己的方式处理异常。因此,当前的行为是经过深思熟虑的并且非常有意义。

如果您仍想在发生即发即弃的任务时立即观察异常,请使用帮助程序 async void 方法:

public static class TaskExt
{
    public static async void Observe(
        this Task @this,
        bool continueOnCapturedContext = true)
    {
        await @this.ConfigureAwait(continueOnCapturedContext);
    } 
}

static void Main(string[] args)
{
    TaskScheduler.UnobservedTaskException += Handler;
    AppDomain.CurrentDomain.UnhandledException += Handler;

    Task.Run(() => { throw new ApplicationException("I'll throw an unhanded exception"); })
        .Observe();

    Task.Factory.StartNew(() => { throw new ApplicationException("I'll throw an unhanded exception too"); })
        .Observe();

    System.Threading.Thread.Sleep(2000);
    Console.WriteLine("I think everything is just peachy!");
    System.Threading.Thread.Sleep(10000);
}

您也可以使用async void lambda 进行即发即弃调用:

Action fireAndForget = async () => 
{ 
    try 
    {
        await Task.Run(...); 
    }
    catch(e) { /* handle errors */ };
};
fireAndForget();

我描述了async void 方法in more details here 的异常传播行为。

好的,我明白你在说什么。按照设计,任务是否意味着永远 与火灾和忘记模式一起使用?只需将工作分派给 以其他方式匿名线程?

我并不是说任务不应该与即发即弃模式一起使用。我实际上认为它们非常适合。有一些选项可以通过任务实现此模式,我在上面展示了一个,另一个是 Task.ContinueWith,或者您可以查看 this question 以获得更多想法。

另一件事是,我几乎无法想象一个有用的场景,我会想要观察任务的完成状态,即使对于像上面这样的即发即弃的调用也是如此。这样的任务会做什么工作?即使对于日志记录,我仍然想确保成功写入日志实体。另外,如果父进程在任务完成之前就结束了怎么办?根据我的经验,我一直使用QueueAsync 之类的东西来跟踪已触发的任务。

【讨论】:

  • 好的,我明白你在说什么。按照设计,任务是否意味着永远不会与“一劳永逸”模式一起使用?
  • @JohnSullivan,我不是这么说的。我更新了答案以解决它。
  • 感谢您的补充信息;我已经开始观察任务是如何在回调中出错的,我很谦虚——在使用 TPL 之后,在任务之外评估任务结果是完全有意义的。
  • 这里还没有解决一些问题。即使您调用 GC.Collect 和 GC.WaitForFinalizers... UnobservedTaskException 事件仍然不会发生。即使你在一个循环中调用了几个小时,它仍然不会发生。为什么未引用的任务永远不会被垃圾收集?还有其他解释吗?
  • @Triynko,嗯 UnobservedTaskException 确实被我调用了,这是一个 simple example
【解决方案2】:

您可以在 app.config 中设置 ThrowUnobservedTaskExceptions 以启用此行为(这是 .NET 4 中的行为)。

请注意,只有在相关Task 被垃圾回收后才会引发异常,因此不会立即发生。这意味着您的测试可能仍然不一定会打印出消息(除非您在此处明确调用 GC.Collect 用于测试目的)。

【讨论】:

  • 即便如此,TaskScheduler.UnobservedTaskException 也只会在任务对象被垃圾回收时触发。
  • @Noseratio 非常正确 - 在这个时间范围内可能会或可能不会发生。编辑以包含该信息。
【解决方案3】:

好吧,这里有一些代码在异步调用任务后调用处理程序(尚未实现处理程序的事件参数),如果您想触发并忘记操作并触发异常事件,只需添加事件参数。

该任务无法在内部知道它是否正在等待,所以我可以理解为什么会这样。我需要做更多的工作和研究来为返回 Task 且可等待的方法实现事件处理程序。如果我能看到这些库的源代码,那就太好了……

void FireAndForgetTaskWithExceptionHandling(Action a)
    {
        Task.Factory.StartNew(() =>
        {
            try
            {
                a.Invoke();
            }
            catch (Exception e)
            {
                Handler(new Object(), new EventArgs());
            }
        }
    );
    }

编辑:如果您要执行上述操作,您基本上只是调用一个操作并调用一个事件处理程序。

((Action)(<your lambda expression here>)).Invoke();

【讨论】:

  • 如果我要使用这种场景,我宁愿使用Task.ContinueWithTaskContinuationOptions.OnlyOnFaulted
  • 感谢 Noseratio。我会看看你在另一篇文章中提到的模式。
猜你喜欢
  • 2021-01-17
  • 1970-01-01
  • 1970-01-01
  • 2023-03-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多