【问题标题】:Getting a meaningful stack trace when using async code使用异步代码时获得有意义的堆栈跟踪
【发布时间】:2015-11-30 21:07:38
【问题描述】:

我创建了一小段代码用于并行运行多个异步操作(Parallel 类本身不适用于异步操作)。

看起来像这样:

public static async Task ForEachAsync<T>(this IEnumerable<T> source, int dop, Func<T, Task> body)
{
    var chunks = source.Chunk(dop);
    foreach (var chunk in chunks)
        await Task.WhenAll(chunk.Select(async s => await body(s).ContinueWith(t => ThrowError(t))));
}

private static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
{
    while (source.Any())
    {
        yield return source.Take(chunksize);
        source = source.Skip(chunksize);
    }
}

private static void ThrowError(Task t)
{
    if (t.IsFaulted)
    {
        if (t.Exception.InnerExceptions != null && t.Exception.InnerExceptions.Count == 1)
            throw t.Exception.InnerExceptions[0];
        else
            throw t.Exception;
    }
}

就并行运行任务而言,上述代码运行良好。但是,当抛出异常时,我确实会遇到一些问题。

就返回异常消息而言,异常捕获代码运行良好,但堆栈跟踪还有很多不足之处 - 因为它指向 ThrowError 方法,而不是最初生成异常的方法。我可以按自己的方式工作并找出附加的调试器出了什么问题,但是如果我发布此应用程序,我将没有该选项可用 - 充其量,我会记录堆栈跟踪的异常。

那么 - 在运行异步任务时,有什么方法可以获得更有意义的堆栈跟踪?

PS。这是针对 WindowsRT 应用程序的,但我认为问题不仅限于 WindowsRT……

【问题讨论】:

  • 不是问题的重点,但Chunk 具有二次时间复杂度,并且由于源的重复枚举而超慢。
  • @usr 也许是这样,但它是一个很好的干净代码,目前,应用程序没有性能问题。我会记住这一点,我可能会在某个时候更新代码。处理的元素数量可能永远不会超过几百个。

标签: c# async-await


【解决方案1】:

那么 - 在运行异步任务时,有什么方法可以获得更有意义的堆栈跟踪?

是的,您可以将 .NET 4.5 中引入的 ExceptionDispatchInfo.Capture 专门用于异步等待:

private static void ThrowError(Task t)
{
    if (t.IsFaulted)
    {
        Exception exception = 
            t.Exception.InnerExceptions != null && t.Exception.InnerExceptions.Count == 1 
                ? t.Exception.InnerExceptions[0] 
                : t.Exception;

        ExceptionDispatchInfo.Capture(exception).Throw();
    }
}

"您可以在另一个时间使用此方法返回的ExceptionDispatchInfo 对象,并可能在另一个线程上重新抛出指定的异常,就好像异常已经从捕获它的点流到它的点被重新抛出。 如果异常在被捕获时处于活动状态,则存储异常中包含的当前堆栈跟踪信息和 Watson 信息。如果处于非活动状态,即没有被抛出,则不会有任何堆栈跟踪信息或 Watson 信息。”

但是,请记住,异步代码中的异常通常没有您希望的那么有意义,因为所有异常都是从编译器生成的状态机上的 MoveNext 方法内部抛出的。

【讨论】:

  • 虽然这不是我在项目最后使用的解决方案(此时它已经很老了),但我将把它标记为答案,因为这似乎是最好的解决方案下一步行动。
【解决方案2】:

i3arnon 的回答是完全正确的,但仍有一些选择。您面临的问题是因为throw 是捕获堆栈跟踪的部分 - 通过再次抛出相同的异常,您已经丢弃了整个堆栈跟踪。

避免这种情况的最简单方法是让Task 完成它的工作:

t.GetAwaiter().GetResult();

就是这样 - 使用正确的堆栈跟踪和所有内容重新抛出异常。您甚至不必检查任务是否出错 - 如果是,它会抛出,如果不是,它不会。

在内部,此方法使用ExceptionDispatchInfo.Capture(exception).Throw(); i3arnon 显示,因此它们几乎等效(您的代码假定任务已经完成,无论是否出错 - 如果尚未完成,@ 987654325@ 将返回 false)。

【讨论】:

  • 我知道使用throw exception 可以消除try-catch 代码中的堆栈跟踪。但是在这种情况下,t.Exception 已经丢失了堆栈跟踪(就像任何InnerExceptions 一样)。所以我再扔一次也没关系 - 信息已经丢失了。
  • @Shaamaan 是的,它只有“真正的”堆栈跟踪,这不是很有用。但是,当您使用t.GetAwaiter().GetResult() 时,会重建“异步”堆栈跟踪。抛出“旧”异常几乎总是一个坏主意——如果你捕捉到GetResult 的结果,你仍然想包装异常(或者重新抛出它而不是抛出)。
  • 我明白了。感谢您清除它。这……有点混乱。如果t.Exception 不包含有意义的异常信息,它有什么用?哦,好吧...
  • @Shaamaan 嗯,它确实包含堆栈跟踪的所有异常信息except。除非您将异常专门用于调试,否则这仍然非常有用 - 例如,如果您期待 SocketException,则不需要关心堆栈跟踪 - 您只想处理异常。您只关心 unhandled 异常的堆栈跟踪,这就是您将获得正确跟踪的地方(除非您自己抛出异常)。但最重要的一点是Task.Exception 早于async/await - MSR 仍然没有时间机器:)
【解决方案3】:

上面的代码效果很好

我不确定您为什么希望将异常直接抛出到线程池 (ContinueWith)。这会使您的进程崩溃,而不会给任何代码清理机会。

对我来说,更自然的方法是让异常冒泡。除了允许自然清理之外,这种方法还删除了所有笨拙、怪异的代码:

public static async Task ForEachAsync<T>(this IEnumerable<T> source, int dop, Func<T, Task> body)
{
  var chunks = source.Chunk(dop);
  foreach (var chunk in chunks)
    await Task.WhenAll(chunk.Select(s => body(s)));
}

private static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
{
  while (source.Any())
  {
    yield return source.Take(chunksize);
    source = source.Skip(chunksize);
  }
}

// Note: No "ThrowError" at all.

【讨论】:

  • 原因是有一个用于执行并行异步操作的框架,并且内置了某种可用的错误处理。由于我没有做太多其他事情,所以我保留了代码。但这是一个很好的观点,我会考虑的。 :)
猜你喜欢
  • 1970-01-01
  • 2014-03-30
  • 2019-08-21
  • 1970-01-01
  • 2011-08-07
  • 2011-11-11
  • 2021-02-25
  • 2018-02-21
  • 1970-01-01
相关资源
最近更新 更多