【问题标题】:Unhandled async exception未处理的异步异常
【发布时间】:2015-12-02 01:07:17
【问题描述】:

除了避免async void的原因,是否有可能从调用代码中捕获以下错误?

private static Action ErrorAction
{
  get
  {
    return async () =>
    {
      await Task.Delay(0);
      throw new NotImplementedException();
    };
  }
}

例如在以下两个中我想收集抛出的异常,但是两个测试都失败了:

[Test]
public void SelfContainedExampleTryCatch()
{
  List<Exception> errors = new List<Exception>();
  try
  {
     ErrorAction();
  }
  catch (Exception ex)
  {
    errors.Add(ex);
  }
  errors.Count().Should().Be(1);
}

[Test]
public void SelfContainedExampleContinueWith()
{
  List<Exception> errors = new List<Exception>();
  var task = Task.Factory.StartNew(ErrorAction);
  task.ContinueWith(t =>
                {
                    errors.Add(t.Exception);
                }, TaskContinuationOptions.OnlyOnFaulted);
  task.Wait();

  errors.Count().Should().Be(1);
}

我知道我可以使用带有async Task 签名的方法;但具体来说,这是我需要处理的 Action 可分配委托异步代码示例,并且在全局级别进行捕获是不切实际的。如果可能的话,同步和异步操作通用的解决方案将是理想的。

希望我错过了一个简单的解决方案,但到目前为止,我只是走入了许多死胡同,并且(可能是错误地)得出结论这是不可能的。任何帮助(或节省时间的浪费)将不胜感激!

【问题讨论】:

  • 测试在什么时候失败?
  • 错误未处理 - 它不会添加到 errors 列表中。
  • @mason 断言 errors.Count().Should().Be(1); 上的测试将失败,因为不会传播异常。
  • 我发现如果我在我的内部处理程序中引入异步代码,我必须使调用代码异步,以及 THAT 的调用代码异步。它最终是异步的。你能解释一下为什么不能使用带有async Task 签名的方法吗?
  • @AndrewShepherd 它不必一直是异步的。您可以通过 var task = MyCallAsync(); task.Wait(); 从未标记为异步的方法中运行异步命令。

标签: c# asynchronous async-await


【解决方案1】:

在某些情况下,框架要求您使用 async void,而有时您真的不想使用(例如,ICommand.Execute)。在这些情况下,我通常建议引入与await 兼容的API,并让您的async void 方法非常简单的包装器:

static Func<Task> ErrorActionAsync
{
  get
  {
    return async () =>
    {
      await Task.Yield();
      throw new NotImplementedException();
    };
  }
}

private static Action ErrorAction
{
  get
  {
    return async () => { await ErrorActionAsync(); }
  }
}

(顺便说一句,我将await Task.Delay(0) 更改为await Task.Yield(),因为await Task.Delay(0) 是一个noop)。

然后您可以在单元测试中使用ErrorActionAsync 而不是ErrorAction。这种方法确实意味着您将拥有少量未经测试的代码(async void 包装器 - ErrorAction)。

但是,如果您想保持ErrorAction 原样,并且可以同步或异步,那么还有另一种可能性。我写了一个AsyncContext type,你可以这样使用:

[Test]
public void SelfContainedExampleTryCatch()
{
  List<Exception> errors = new List<Exception>();
  try
  {
    AsyncContext.Run(() => ErrorAction());
  }
  catch (Exception ex)
  {
    errors.Add(ex);
  }
  errors.Count().Should().Be(1);
}

AsyncContext.Run 将阻塞直到async void 方法完成,并将捕获来自async void 方法的异常并直接引发它们。

【讨论】:

    【解决方案2】:

    如果您希望 ErrorAction 能够满足异步调用,然后仍然能够在调用站点处理异常,那么我认为您只需要硬着头皮将 ErrorAction 更改为返回 Task。

    如果您决定更改合同,最好保持现有非异步实现的同步特性。你可以这样做:

    private static Task ErrorAction
    {
      get
      {
        // Synchronous code here
    
        return Task.FromResult(true);
      }
    }
    

    【讨论】:

      【解决方案3】:

      我认为在不更改合同的情况下返回 Task 是可能的。看这个例子:

      using System;
      using System.Collections.Generic;
      using System.Linq;
      using System.Threading.Tasks;
      using FluentAssertions;
      
      namespace ConsoleApplication1
          {
          class Program
              {
              static void Main(string[] args)
                  {
                  SelfContainedExampleTryCatch();
                  SelfContainedExampleContinueWith();
                  Console.WriteLine("Press any key to exit.");
                  Console.ReadKey();
                  }
      
              public static void SelfContainedExampleTryCatch()
                  {
                  var errors = new List<Exception>();
                  try
                      {
                      ErrorAction();
                      }
                  catch (Exception ex)
                      {
                      errors.Add(ex);
                      }
                  errors.Count().Should().Be(1);
                  }
      
              public static void SelfContainedExampleContinueWith()
                  {
                  var errors = new List<Exception>();
                  var task = Task.Factory.StartNew(ErrorAction);
                  task.ContinueWith(t =>
                  {
                      errors.Add(t.Exception);
                  }, TaskContinuationOptions.OnlyOnFaulted);
                  task.Wait();
      
                  errors.Count().Should().Be(1);
                  }
      
              private static Action ErrorAction
                  {
                  get
                      {
                      return () => DoTask().Wait();
                      }
                  }
      
              private static async Task DoTask() {
                  await Task.Delay(0);
                  throw new NotImplementedException();
                  }
              }
          }
      

      ContinueWith 测试通过,但其他测试不会。我相信这是你的意图。

      【讨论】:

      • @lonewolf 我相信没有办法解决这个问题。如果您不希望它被阻止,那么您需要更改合同。
      • 问题是我有一个接受动作的公开合同 - 我无法控制第三方传入的内容,因此重新排列输入,除非我可以在接受的动作和调用之间放置一些东西代码不会成功,不过还是感谢您的建议。
      猜你喜欢
      • 1970-01-01
      • 2019-08-29
      • 2018-10-15
      • 1970-01-01
      • 1970-01-01
      • 2022-10-14
      • 2017-06-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多