未处理的异步异常

Posted

技术标签:

【中文标题】未处理的异步异常【英文标题】: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(); 从未标记为异步的方法中运行异步命令。 【参考方案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 我相信没有办法解决这个问题。如果您不希望它被阻止,那么您需要更改合同。 问题是我有一个接受动作的公开合同 - 我无法控制第三方传入的内容,因此重新排列输入,除非我可以在接受的动作和调用之间放置一些东西代码不会成功,不过还是感谢您的建议。

以上是关于未处理的异步异常的主要内容,如果未能解决你的问题,请参考以下文章

来自回调 c# 的未处理异常错误

Try/catch 块和未处理的承诺异常

在 TPL 中快速抛出未处理的异常

ajax异步提交 有时会出现无bug的数据处理异常-----debug没有问题,正常运行却数据处理不正确,极少机会会出现正常的处理结果

C#中异步编程多个异常的处理方式

javascript中异步操作的异常怎么处理