为什么在使用异步等待时尽管观察到仍抛出UnobservedTaskException?

时间:2018-07-18 11:11:52

标签: c# .net task task-parallel-library

以下情形在 .NET 4.5 下运行,因此任何UnobservedTaskException都不terminate the process

我习惯于在应用程序的开头执行此操作,以监听任何抛出的UnobservedTaskException

private void WatchForUnobservedTaskExceptions()
{
  TaskScheduler.UnobservedTaskException += (sender, args) =>
  {
      args.Exception.Dump("Ooops");
  };
}

当我想显式忽略任务抛出的任何异常时,我还有一个辅助方法:

public static Task IgnoreExceptions(Task task) 
  => task.ContinueWith(t =>
      {
          var ignored = t.Exception.Dump("Checked");
      },
      CancellationToken.None,
      TaskContinuationOptions.ExecuteSynchronously,
      TaskScheduler.Default);

因此,如果我有以下代码,请执行:

void Main()
{
  WatchForUnobservedTaskExceptions();

  var task = Task.Factory.StartNew(() =>
  {
      Thread.Sleep(1000);
      throw new InvalidOperationException();
  });

  IgnoreExceptions(task);

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

  Console.ReadLine();    
}

Console.ReadLine()返回后,我们将看不到任何UnobservedTaskException被抛出的情况。

但是,如果我将上述task更改为开始使用async/await,则其他一切与以前相同:

var task = Task.Factory.StartNew(async () =>
{
    await Task.Delay(1000);
    throw new InvalidOperationException();
});

现在,我们将抛出UnobservedTaskException。调试代码可以发现t.Exceptionnull时继续执行。

在两种情况下如何正确忽略异常?

2 个答案:

答案 0 :(得分:4)

可以使用

var task = Task.Factory.StartNew(async () =>
{
    await Task.Delay(1000);
    throw new InvalidOperationException();
}).Unwrap();

var task = Task.Run(async () =>
{
    await Task.Delay(1000);
    throw new InvalidOperationException();
});

请参见this blogpost about Task.Run vs Task.Factory.StartNew,了解如何将Task.Factory.StartNew与异步修饰符一起使用

  

通过在此处使用async关键字,编译器将映射此委托为Func<Task<int>>:调用委托将返回Task<int>,以表示此调用的最终完成。并且由于委托是Func<Task<int>>,所以TResultTask<int>,因此't'的类型将是Task<Task<int>>,而不是Task<int>。      

为处理此类情况,在.NET 4中,我们引入了Unwrap方法。

更多background

  

为什么不使用Task.Factory.StartNew?

     

..不了解异步委托。 ……。问题在于,当您将异步委托传递给StartNew时,自然会假定返回的任务代表该委托。但是,由于StartNew无法理解异步委托,因此该任务实际代表的只是该委托的开始。这是编码人员在异步代码中使用StartNew时遇到的第一个陷阱。

编辑

task =>中var task = Task.Factory.StartNew(async (...))的类型实际上是Task<Task<int>>。您必须Unwrap才能获得源任务。考虑到这一点:

您只能在Unwrap上调用Task<Task>>,因此可以在IgnoreExceptions上添加一个重载来容纳它:

void Main()
{
    WatchForUnobservedTaskExceptions();

    var task = Task.Factory.StartNew(async () =>
    {
        await Task.Delay(1000);
        throw new InvalidOperationException();
    });

    IgnoreExceptions(task);

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

    Console.ReadLine();
}

private void WatchForUnobservedTaskExceptions()
{
    TaskScheduler.UnobservedTaskException += (sender, args) =>
    {
        args.Exception.Dump("Ooops");
    };
}

public static Task IgnoreExceptions(Task task)
  => task.ContinueWith(t =>
      {
          var ignored = t.Exception.Dump("Checked");
      },
      CancellationToken.None,
      TaskContinuationOptions.ExecuteSynchronously,
      TaskScheduler.Default);


public static Task IgnoreExceptions(Task<Task> task)
=> task.Unwrap().ContinueWith(t =>
{
    var ignored = t.Exception.Dump("Checked");
},
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);

答案 1 :(得分:2)

varTask的结合以及Task<T>的关联关系掩盖了问题。如果我稍微重写一下代码,问题出在哪里就会变得很明显。

  Task<int> task1 = Task.Factory.StartNew(() =>
  {
     Thread.Sleep(1000);
     throw new InvalidOperationException();
     return 1;
  });

  Task<Task<int>> task2 = Task.Factory.StartNew(async () =>
  {
     await Task.Delay(1000);
     throw new InvalidOperationException();
     return 1;
  });

这更好地说明了Peter Bons在说什么。