使用异步代码时获取有意义的堆栈跟踪

时间:2015-11-30 09:02:50

标签: c# async-await

我已经创建了一小段代码来并行运行多个异步操作(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 ...

3 个答案:

答案 0 :(得分:26)

  

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

是的,您可以使用.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();
    }
}
  

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

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

答案 1 :(得分:12)

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

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

t.GetAwaiter().GetResult();

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

在内部,此方法使用ExceptionDispatchInfo.Capture(exception).Throw(); i3arnon已显示,因此它们几乎等效(您的代码假定任务已经完成,出现故障或不出现 - 如果它&#39;尚未完成,IsFaulted将返回false)。

答案 2 :(得分:5)

  

上面的代码效果很好

我不确定为什么要直接在线程池(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.