我希望await能抛出AggregateException,而不仅仅是第一个异常。

24
当等待一个出错的任务(即已设置异常的任务)时,await将重新抛出存储的异常。如果存储的异常是AggregateException,它将重抛第一个异常并丢弃其余的异常。
我们如何在使用await的同时抛出原始的AggregateException,以便不会意外丢失错误信息?
请注意,当然可以考虑一些hacky解决方案(例如,在await周围放置try-catch,然后调用Task.Wait)。我真的希望找到一个简洁的解决方案。在这里,什么是最佳实践?
我想使用自定义awaiter,但内置的TaskAwaiter包含许多神奇的内容,我不确定如何完全复制。它在TPL类型上调用内部API。我也不想再次编写所有这些内容。
以下是一个简短的示例代码:
static void Main()
{
    Run().Wait();
}

static async Task Run()
{
    Task[] tasks = new[] { CreateTask("ex1"), CreateTask("ex2") };
    await Task.WhenAll(tasks);
}

static Task CreateTask(string message)
{
    return Task.Factory.StartNew(() => { throw new Exception(message); });
}

Run中,只会抛出两个异常中的一个。

请注意,Stack Overflow上的其他问题并没有解决这个具体的问题。在建议重复内容时请小心。


3
你看过这篇博客文章吗?它解释了为什么他们选择以这种方式实现await的异常处理。http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/10217876.aspx - Matthew Watson
1
是的,谢谢。我有点同意一般情况下的推理,但我的用例没有涉及到。 - usr
在所有情况下,任务的异常属性仍然返回一个包含所有异常的AggregateException,因此您可以捕获抛出的任何异常并在需要时返回查看Task.Exception。 - Tim Sparkles
相关的 GitHub API 提案:通过抛出 AggregateException 配置 await 以传播所有错误 - Theodor Zoulias
这里有一个解决方案,它保留了所有异常并正确地传播了取消状态:https://dev59.com/xmct5IYBdhLWcg3wjuAj#62607500 - noseratio - open to work
1
这是一篇由Matthew Watson最初分享的博客文章的更新链接:https://devblogs.microsoft.com/pfxteam/task-exception-handling-in-net-4-5/ - Holistic Developer
6个回答

28

我不同意你在问题标题中的暗示,即await的行为是不受欢迎的。在绝大多数情况下它都是有意义的。在WhenAll的情况下,你到底有多少次需要知道所有错误细节,而不只是其中一个呢?

AggregateException 的主要问题在于异常处理,也就是说,你失去了捕获特定类型异常的能力。

话虽如此,你可以使用扩展方法来获得你想要的行为:

public static async Task WithAggregateException(this Task source)
{
  try
  {
    await source.ConfigureAwait(false);
  }
  catch
  {
    // source.Exception may be null if the task was canceled.
    if (source.Exception == null)
      throw;

    // EDI preserves the original exception's stack trace, if any.
    ExceptionDispatchInfo.Capture(source.Exception).Throw();
  }
}

13
获取所有异常对诊断很重要。我不想错过空引用等异常,它们必须被记录下来。然而处理异常是一个不同的情况。顺便说一下,这个解决方案运行良好。谢谢。 - usr
2
如果你在谈论致命或愚蠢的异常,我建议使用顶层处理程序(UnobservedTaskException、应用程序处理程序),而不是重复使用 try/catch 块。 - Stephen Cleary
2
我基本上同意。我心中所想的情景确实是一个顶级错误处理程序(它只记录崩溃)。但完整的崩溃信息必须以某种方式到达那里。在存在await的情况下,UnobservedTaskException和Application处理程序不能保证这一点,因为错误可能会被观察到但被吞噬。当我看到错误被吞噬时,我会非常紧张。我见过太多生产环境遭受严重错误数月之久而没有人注意到的情况。 - usr
2
对于未观察到的异常,仍会引发 UnobservedTaskException(只是不再使进程崩溃)。唯一出现已观察且被忽略的异常情况是在使用 WhenAll 时,只有其中一个异常 没有 被忽略。使用 await 必须确实尝试忽略异常。 - Stephen Cleary
3
请注意,取消操作期间 source.Exception 可能是 null 引用。 - tm1
显示剩余13条评论

6

这是Stephen Cleary的WithAggregateException扩展方法的简短实现:

public static async Task WithAggregateException(this Task source)
{
    try { await source.ConfigureAwait(false); }
    catch when (source.IsCanceled) { throw; }
    catch { source.Wait(); }
}

public static async Task<T> WithAggregateException<T>(this Task<T> source)
{
    try { return await source.ConfigureAwait(false); }
    catch when (source.IsCanceled) { throw; }
    catch { return source.Result; }
}

这种方法基于 Stephen Toub 在 GitHub 上 this API 提议中的建议。
更新:我添加了一个特殊处理取消情况的方法,以避免传播包含OperationCanceledExceptionAggregateException所带来的尴尬。现在OperationCanceledException直接传播,并且Task.IsCanceled状态得到保留。感谢@noseratio在this答案的评论中指出这个缺陷。当然,现在这个实现并不比Stephen Cleary的方法短!

@ConstantLearner 这不是 Stephen Cleary 答案的修改版本。这是一个不同的实现,也恰好更短一些。关于 ExceptionDispatchInfo,在我的实现中不需要它,因为我没有直接 throw 任何异常。异常是通过调用 Task.Wait 方法间接抛出的。我认为在 Stephen Cleary 的实现中也不需要它。Task.Exception 属性只是 InnerExceptions 的容器。它本身没有堆栈跟踪。所以 throw source.Exception; 应该是相同的。 - Theodor Zoulias

5

我知道我来晚了,但我发现一个很棒的小技巧可以实现你想要的功能。由于完整的异常集合可以通过等待的任务获得,调用该任务的Wait或.Result将抛出聚合异常。

    static void Main(string[] args)
    {
        var task = Run();
        task.Wait();
    }
    public static async Task Run()
    {

        Task[] tasks = new[] { CreateTask("ex1"), CreateTask("ex2") };
        var compositeTask = Task.WhenAll(tasks);
        try
        {
            await compositeTask.ContinueWith((antecedant) => { }, TaskContinuationOptions.ExecuteSynchronously);
            compositeTask.Wait();
        }
        catch (AggregateException aex)
        {
            foreach (var ex in aex.InnerExceptions)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }

    static Task CreateTask(string message)
    {
        return Task.Factory.StartNew(() => { throw new Exception(message); });
    }

3

异常处理(任务并行库)

我可以说更多,但那只是填充。尝试一下,它确实像他们所说的那样工作。你只需要小心。

也许你想要这个

上帝(Jon Skeet)解释了等待异常处理

(个人而言,我避开await,但这只是我的偏好)

针对评论的回应(过长无法在评论中回复)

然后将线程用作类比论据的起点,因为最佳实践将是此处的源。除非您实现代码将其传递出去(例如,await预计包装的异步模式...当您引发事件时,在事件参数对象中添加它们)。当您启动任意数量的线程并在其中执行时,您无法控制顺序或终止每个线程的时间点。此外,如果一个错误与另一个相关,则永远不会使用此模式。因此,您强烈暗示这些线程上的执行已完全独立 - 即您强烈暗示这些线程上的异常已经被处理为异常。如果您想在这些线程中以外的地方做一些超出处理这些线程中的异常的事情(这很奇怪),则应将它们添加到通过引用传递的锁定集合中 - 您不再将异常视为异常,而是作为一条信息 - 使用并发包,在其中包装异常及其所需的标识其来自的上下文的信息 - 这些信息将被传递给它。

不要混淆您的用例。


第一个链接与await无关。第二个链接包含了一个有效的答案。谢谢。我会等待几天以便发现最佳解决方案。 - usr
事实是我不同意你的用例。我认为当你使用await时,你假设每个任务都处理自己的异常-await告诉你,在一个操作中出现了错误,你不能取消其他任务(即它们必须是独立的)。或者至少我认为这是它的模式所假定的。如果你想在任务外处理异常,那么你应该使用顶级模式。我可能应该解释一下。另外,如果你想要聚合异常-问题-第一个应该是你的选择。 - John Nicholas
顶部模式涉及阻塞,这是与等待地址完全不同的用例。这是一个有效的用例,但不是我的 - 我需要异步IO和非阻塞;此外,任务不一定处理自己的错误。如果是这样的话,我们永远不需要错误传播。错误应该传播,以便中止正常的控制流程。我想要的只是所有错误,而不仅仅是任意一个。 - usr
更新...当然,任务处理自己的错误...任务是封装工作单元的封装单元。我认为处理其异常的工作绝对是其中的一部分,否则您将为自己创建100个线程都抛出异常并在一段代码上阻塞的锁定噩梦 - 除非您使其没有状态,此时您会意识到它本质上就坐落在任务本身中...或者与我妥协,并传递一个lambda来处理异常。 - John Nicholas
@JohnNicholas 的参考文章链接已经失效。 - Konstantin Chernov
https://codeblog.jonskeet.uk/2011/06/22/eduasync-part-11-more-sophisticated-but-lossy-exception-handling/ .... 我有时候真是个有意见的人。 - John Nicholas

1

我不想放弃练习,仅仅为了捕获我预期的异常。这促使我编写了以下扩展方法:

public static async Task NoSwallow<TException>(this Task task) where TException : Exception {
    try {
        await task;
    } catch (TException) {
        var unexpectedEx = task.Exception
                               .Flatten()
                               .InnerExceptions
                               .FirstOrDefault(ex => !(ex is TException));
        if (unexpectedEx != null) {
            throw new NotImplementedException(null, unexpectedEx);
        } else {
            throw task.Exception;
        }
    }
}

消费代码可能是这样的:

try {
    await Task.WhenAll(tasks).NoSwallow<MyException>();
catch (AggregateException ex) {
    HandleExceptions(ex);
}

即使在并发地抛出一个MyException的情况下,一个骨头头脑异常会产生与同步世界相同的影响。使用NotImplementedException进行包装有助于不丢失原始堆栈跟踪。


0

此扩展将原始聚合异常进行包装,不改变其返回类型,因此仍可与 Task<T> 一同使用。

 public static Task<T> UnswallowExceptions<T>(this Task<T> t)
        => t.ContinueWith(t => t.IsFaulted ? throw new AggregateException("whatever", t.Exception) : t.Result);

例子:

Task<T[]> RunTasks(Task<T>[] tasks) => 
Task.WhenAll(CreateSometasks()).UnswallowExceptions();
try
{ var result = await CreateTasks(); }
catch(AggregateException ex) {  } //ex is original aggregation exception here

注意 如果任务被取消,此方法将抛出异常。如果取消对您很重要,请使用另一种方法。


ContinueWith是一种基本方法,在使用时应特别小心,尤其是与async/await代码结合使用时。此外,如果t被取消了怎么办?UnswallowExceptions方法如何处理这种情况? - Theodor Zoulias
它显然会抛出异常。我将在答案中更新这个信息。 - Vitaly
我认为它不会抛出异常。它可能会返回一个任务,该任务永远无法转换为“已取消”状态。顺便说一句,尽管我没有测试过你的方法,但我认为它应该与这个更短的版本类似:=> t.ContinueWith(t => t.Result); - Theodor Zoulias
在更新答案之前,我进行了测试,当访问t.Result时它会抛出TaskCanceledException,然后被包装在AggregationException中,因此您将失去原始异常并仅捕获带有TaskCanceledException的AggregationException。在访问t.Result之前可以添加对t.IsCanceled的检查,但是这样会得到错误的任务结果而不是取消的任务。如果您需要所有这些内容,最好检查John Skeet的解决方案,它涵盖了所有内容,但是在每次调用时创建TaskCompletionSource有点过度。 - Vitaly
为什么不使用Stephen Cleary的解决方案呢?它很简短,而且似乎可以正确处理所有情况。你是否因为某些原因想要避免使用async/await? - Theodor Zoulias
检查了您的较短版本,它类似于throw t.Exception,如果只使用ex.Flatten().InnerExceptions,则更好,因为您不会创建新的异常。但是堆栈跟踪和源已更改,在这种情况下不是非常重要,但可能不希望这样。 - Vitaly

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接