是否可以使用.NET异步方法获得良好的堆栈跟踪?

Cha*_*ion 52 c# stack-trace async-await asp.net-web-api

我在WebApi应用程序中设置了以下示例代码:

[HttpGet]
public double GetValueAction()
{
    return this.GetValue().Result;
}

public async Task<double> GetValue()
{
    return await this.GetValue2().ConfigureAwait(false);
}

public async Task<double> GetValue2()
{
    throw new InvalidOperationException("Couldn't get value!");
}
Run Code Online (Sandbox Code Playgroud)

可悲的是,当GetValueAction被击中时,返回的堆栈跟踪是:

    " at MyProject.Controllers.ValuesController.<GetValue2>d__3.MoveNext() in c:\dev\MyProject\MyProject\Controllers\ValuesController.cs:line 61 --- End of stack trace from previous location where exception was thrown --- 
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) 
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) 
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
at MyProject.Controllers.ValuesController.<GetValue>d__0.MoveNext() in c:\dev\MyProject\MyProject\Controllers\ValuesController.cs:line 56"
Run Code Online (Sandbox Code Playgroud)

因此,我在跟踪中得到(损坏)GetValue2和GetValue,但没有提到GetValueAction.难道我做错了什么?是否有另一种模式可以让我获得更完整的堆栈跟踪?

编辑:我的目标不是编写依赖于堆栈跟踪的代码,而是使异步方法中的失败更容易调试.

Ste*_*ary 36

首先,堆栈跟踪并不像大多数人认为的那样.它们在调试期间非常有用,但不适合运行时使用,特别是在ASP.NET上.

此外,堆栈跟踪在技术上是关于代码返回的位置,而不是代码来自何处.使用简单(同步)代码,两者是相同的:代码总是返回到任何称为它的方法.但是,对于异步代码,这两者是不同的.再次,堆栈跟踪告诉你会发生什么未来,但你有兴趣在什么事过去.

因此,堆栈框架不是满足您需求的正确答案.Eric Lippert在他的回答中解释了这一点.

MSDN文章说@ColeCampbell链接到介绍追踪"因果链"(其中的代码来了一个方式)与async代码.不幸的是,这种方法是有限的(例如,它不处理fork/join方案); 但是,这是我所知道的唯一可以在Windows应用商店应用中运行的方法.

由于您使用完整的.NET 4.5运行时使用ASP.NET,因此您可以访问用于跟踪临时链的更强大的解决方案:逻辑调用上下文.但是,您的async方法必须"选择加入",因此您不会像使用堆栈跟踪那样免费获取它.我刚刚在一篇尚未发布的博客文章中写到这一点,所以你正在预览.:)

您可以围绕逻辑调用上下文构建一个"堆栈"调用,如下所示:

public static class MyStack
{
  // (Part A) Provide strongly-typed access to the current stack
  private static readonly string slotName = Guid.NewGuid().ToString("N");
  private static ImmutableStack<string> CurrentStack
  {
    get
    {
      var ret = CallContext.LogicalGetData(name) as ImmutableStack<string>;
      return ret ?? ImmutableStack.Create<string>();
    }
    set { CallContext.LogicalSetData(name, value); }
  }

  // (Part B) Provide an API appropriate for pushing and popping the stack
  public static IDisposable Push([CallerMemberName] string context = "")
  {
    CurrentStack = CurrentStack.Push(context);
    return new PopWhenDisposed();
  }
  private static void Pop() { CurrentContext = CurrentContext.Pop(); }
  private sealed class PopWhenDisposed : IDisposable
  {
    private bool disposed;
    public void Dispose()
    {
      if (disposed) return;
      Pop();
      disposed = true;
    }
  }

  // (Part C) Provide an API to read the current stack.
  public static string CurrentStackString
  {
    get { return string.Join(" ", CurrentStack.Reverse()); }
  }
}
Run Code Online (Sandbox Code Playgroud)

(ImmutableStack在这里获得).然后你可以像这样使用它:

static async Task SomeWork()
{
  using (MyStack.Push())
  {
    ...
    Console.WriteLine(MyStack.CurrentStackAsString + ": Hi!");
  }
}
Run Code Online (Sandbox Code Playgroud)

这种方法的好处在于它适用于所有 async代码:fork/join,custom awaitables ConfigureAwait(false)等.缺点是你增加了一些开销.此外,此方法仅适用于.NET 4.5 ; .NET 4.0上的逻辑调用上下文不是 - async并且无法正常工作.

更新:我发布了一个NuGet包(在我的博客上描述),它使用PostSharp自动注入推送和弹出.因此,获得良好的痕迹现在应该更加简单.

  • 谢谢Stephen.我将添加关于不可靠调用堆栈的说明,在内联优化或尾调用优化的情况下,调用堆栈顶部的东西可能是*调用者的*调用者.所以你甚至不能在许多"简单"场景中依赖它. (4认同)

Dou*_*las 10

这个问题及其最高投票答案都是在2013年写回来的.从那以后情况有所改善.

.NET Core 2.1现在提供开箱即用的可理解的异步堆栈跟踪; 请参阅.NET Core 2.1中的Stacktrace改进.

对于那些仍在.NET Framework上的人来说,有一个优秀的NuGet包可以修复堆栈跟踪中的异步(以及许多其他的瑕疵):Ben.Demystifier.该软件包优于其他建议的优点是它不需要更改抛出代码或程序集; 你只需要调用DemystifyToStringDemystified捕获被捕异常.

将此应用于您的代码:

System.AggregateException: One or more errors occurred. ---> System.InvalidOperationException: Couldn't get value!
   at async Task<double> ValuesController.GetValue2()
   at async Task<double> ValuesController.GetValue()
   --- End of inner exception stack trace ---
   at void System.Threading.Tasks.Task.ThrowIfExceptional(bool includeTaskCanceledExceptions)
   at TResult System.Threading.Tasks.Task<TResult>.GetResultCore(bool waitCompletionNotification)
   at TResult System.Threading.Tasks.Task<TResult>.get_Result()
   at double ValuesController.GetValueAction()
   at void Program.Main(string[] args)
---> (Inner Exception #0) System.InvalidOperationException: Couldn't get value!
   at async Task<double> ValuesController.GetValue2()
   at async Task<double> ValuesController.GetValue()<---
Run Code Online (Sandbox Code Playgroud)

由于你的使用,这无疑是有点复杂的Task<T>.Result.如果你将你的GetValueAction方法转换为异步(一直都是异步的精神),你会得到预期的干净结果:

System.InvalidOperationException: Couldn't get value!
   at async Task<double> ValuesController.GetValue2()
   at async Task<double> ValuesController.GetValue()
   at async Task<double> ValuesController.GetValueAction()
Run Code Online (Sandbox Code Playgroud)

  • 对于行号,您需要包含 .pdb 文件(或嵌入式调试信息) (2认同)

Yep*_*kai 5

async/await king 为此提供了一个很好的 nuget 扩展。

https://www.nuget.org/packages/AsyncStackTraceEx/

您需要更改您的等待呼叫

Await DownloadAsync(url)
Run Code Online (Sandbox Code Playgroud)

Await DownloadAsync(url).Log()
Run Code Online (Sandbox Code Playgroud)

最后,在 catch 块中,只需调用

ex.StackTraceEx()
Run Code Online (Sandbox Code Playgroud)

一个重要的注意事项:此方法只能调用一次,并且之前不得评估 ex.StackTrace。看来堆栈只能读取一次。