【问题标题】:Is it possible to get a good stack trace with .NET async methods?是否可以使用 .NET 异步方法获得良好的堆栈跟踪?
【发布时间】:2013-03-02 21:12:59
【问题描述】:

我在 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!");
}

可悲的是,当 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"

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

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

【问题讨论】:

  • 你可能想看看异步因果链上的this article
  • @ChaseMedallion:请查看我的更新答案。
  • 不是直接的答案,但this answer 给出了如何破译给定异步堆栈帧的原始方法的提示。

标签: c# asp.net-web-api stack-trace async-await


【解决方案1】:

首先,堆栈跟踪并不像大多数人认为的那样。它们在调试过程中很有用,但不适合运行时使用,尤其是在 ASP.NET 上。

此外,堆栈跟踪在技术上是关于代码返回到哪里,而不是代码来自哪里。对于简单(同步)代码,两者是相同的:代码总是返回到调用它的任何方法。但是,对于异步代码,这两者是不同的。同样,堆栈跟踪告诉您下一个会发生什么,但您对过去发生的事情感兴趣。

因此,堆栈帧不是您需要的正确答案。 Eric Lippert explains this well in his answer here.

@ColeCampbell 链接到的MSDN article 描述了一种使用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()); }
  }
}

ImmutableStack 可用here)。然后你可以像这样使用它:

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

这种方法的好处是它适用于 all async 代码:fork/join、自定义等待对象、ConfigureAwait(false) 等。缺点是您增加了一些开销.此外,这种方法仅适用于 .NET 4.5; .NET 4.0 上的逻辑调用上下文不是async-aware 并且将正常工作。

更新:我发布了一个NuGet package (described on my blog),它使用 PostSharp 自动注入推送和弹出。所以现在获得良好的跟踪应该容易得多。

【讨论】:

  • 谢谢斯蒂芬。我想在你的注释中添加关于不可靠调用堆栈的内容,在内联优化或尾调用优化的情况下,调用堆栈顶部的东西可能是 caller's 的调用者。所以在很多“简单”的场景中你甚至不能依赖它。
【解决方案2】:

这个问题及其最高票数的答案写于 2013 年。从那时起情况有所改善。

.NET Core 2.1 现在提供开箱即用的可理解的异步堆栈跟踪;见Stacktrace improvements in .NET Core 2.1

对于仍在使用 .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()<---

诚然,由于您使用了Task&lt;T&gt;.Result,这仍然有点令人费解。如果您将 GetValueAction 方法转换为异步(本着 async all the way 的精神),您将获得预期的干净结果:

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

【讨论】:

  • Xamarin.Forms.UWP 上仍未显示行号
  • 对于行号,您需要包含 .pdb 文件(或嵌入的调试信息)
【解决方案3】:

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

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

你需要改变你的等待电话

Await DownloadAsync(url)

Await DownloadAsync(url).Log()

最后,在catch块中,调用

ex.StackTraceEx()

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

【讨论】:

    猜你喜欢
    • 2014-03-30
    • 2018-07-19
    • 2015-11-09
    • 2021-11-03
    • 1970-01-01
    • 2014-11-24
    • 2017-04-01
    • 1970-01-01
    • 2019-12-01
    相关资源
    最近更新 更多