为什么在使用 async await 时抛出 UnobservedTaskException?

Why is UnobservedTaskException thrown despite the observation when using async await?

以下场景在 .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() return 之后,我们将不会看到任何 UnobservedTaskException 抛出,这正是我们所期望的。

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

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

现在我们抛出了 UnobservedTaskException。调试代码显示继续执行 t.Exceptionnull.

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

要么使用

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 与异步修饰符一起使用的信息

By using the async keyword here, the compiler is going to map this delegate to be a Func<Task<int>>: invoking the delegate will return the Task<int> to represent the eventual completion of this call. And since the delegate is Func<Task<int>>, TResult is Task<int>, and thus the type of ‘t’ is going to be Task<Task<int>>, not Task<int>.

To handle these kinds of cases, in .NET 4 we introduced the Unwrap method.

还有一些background

Why Not to Use Task.Factory.StartNew?

.. Does not understand async delegates. … . The problem is that when you pass an async delegate to StartNew, it’s natural to assume that the returned task represents that delegate. However, since StartNew does not understand async delegates, what that task actually represents is just the beginning of that delegate. This is one of the first pitfalls that coders encounter when using StartNew in async code.

编辑

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

您只能在 Task<Task>> 上调用 Unwrap,因此您可以向 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);

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 所说的内容。