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

C# 使用异步代码时获取有意义的堆栈跟踪,c#,async-await,C#,Async Await,我创建了一小段代码,用于并行运行多个异步操作(类本身不适合异步操作) 看起来是这样的: 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

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

看起来是这样的:

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;
    }
}
公共静态异步任务ForEachAsync(此IEnumerable源代码,int-dop,Func-body)
{
var chunks=source.Chunk(dop);
foreach(var chunk in chunks)
wait Task.WhenAll(chunk.Select(异步s=>wait body.ContinueWith(t=>ThrowError(t)));
}
私有静态IEnumerable块(此IEnumerable源,int chunksize)
{
while(source.Any())
{
收益返回源。获取(chunksize);
source=source.Skip(chunksize);
}
}
专用静态空隙喷射器(任务t)
{
如果(t.IsFaulted)
{
if(t.Exception.InnerExceptions!=null&&t.Exception.InnerExceptions.Count==1)
抛出t.Exception.InnerExceptions[0];
其他的
抛出t异常;
}
}
就并行运行任务而言,上面的代码工作得非常好。但是,当抛出异常时,我确实有一些问题

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

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

另外,这是针对WindowsRT应用程序的,但我认为问题不限于WindowsRT本身

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

是的,您可以使用.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
方法中抛出的。

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

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

t.GetAwaiter().GetResult();
就是这样-异常是使用正确的堆栈跟踪和所有内容重新启动的。您甚至不必检查任务是否有错误——如果有,它将抛出,如果没有,它将不会抛出

在内部,此方法使用
ExceptionDispatchInfo.Capture(exception.Throw()i3arnon已显示,因此它们几乎相等(您的代码假定任务已完成,有无故障-如果尚未完成,
IsFaulted
将返回false)

上面的代码工作得非常好

我不确定您为什么希望在线程池中直接抛出异常(
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.
公共静态异步任务ForEachAsync(此IEnumerable源代码,int-dop,Func-body)
{
var chunks=source.Chunk(dop);
foreach(var chunk in chunks)
wait Task.WhenAll(chunk.Select(s=>body));
}
私有静态IEnumerable块(此IEnumerable源,int chunksize)
{
while(source.Any())
{
收益返回源。获取(chunksize);
source=source.Skip(chunksize);
}
}
//注意:根本没有“投掷者”。

我知道使用
抛出异常
可以消除
try catch
代码中的堆栈跟踪。但是在本例中,
t.Exception
已经缺少堆栈跟踪(与任何
InnerExceptions
一样)。所以,如果我再扔一次也没关系——信息已经丢失了。@Shaaman是的,它只有“真实的”堆栈跟踪,这不是很有用。但是,当您使用
t.GetAwaiter().GetResult()
时,将重建“异步”堆栈跟踪。抛出“旧”异常几乎总是一个坏主意-如果捕获了
GetResult
的结果,您仍然希望包装异常(或者重新抛出异常而不是抛出异常)。我明白了。谢谢你澄清这一点。是。。。有点困惑。如果它不包含有意义的异常信息,那么
t.Exception
有什么好处?哦,好吧…@shaaman好吧,它确实包含除了堆栈跟踪之外的所有异常信息。除非您专门使用异常进行调试,否则这仍然非常有用—例如,如果您希望出现
SocketException
,则不关心堆栈跟踪—您只想处理异常。您只关心未处理异常的堆栈跟踪,而这正是获得正确跟踪的地方(除非您自己抛出异常)。但最重要的一点是,
Task.Exception
早于
async
/
wait
-MSR仍然没有时间机器:)这不是重点