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

Sha*_*aan 30 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

PS.这适用于WindowsRT应用程序,但我认为问题不仅限于WindowsRT ......

i3a*_*non 26

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

是的,您可以使用ExceptionDispatchInfo.Capture.NET 4.5中引入的async-await特别注意:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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


Lua*_*aan 12

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

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

t.GetAwaiter().GetResult();
Run Code Online (Sandbox Code Playgroud)

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

在内部,这个方法使用ExceptionDispatchInfo.Capture(exception).Throw();i3arnon已经显示,所以它们几乎是等价的(你的代码假设任务已经完成,出现故障或者没有 - 如果它还没有完成,IsFaulted将返回false).


Ste*_*ary 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.
Run Code Online (Sandbox Code Playgroud)