web-dev-qa-db-ja.com

.NET非同期メソッドで適切なスタックトレースを取得することは可能ですか?

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については言及しません。私は何か間違っていますか?より完全なスタックトレースを取得する別のパターンはありますか?

編集:私の目標は、スタックトレースに依存するコードを書くことではなく、非同期メソッドの失敗をデバッグしやすくすることです。

49
ChaseMedallion

この質問とその最高得票数の回答は2013年に書き戻されました。その後、状況は改善されました。

.NET Core 2.1はすぐに理解できる非同期スタックトレースを提供します。 。NET Core 2.1でのStacktraceの改善 を参照してください。

まだ.NET Frameworkを使用している場合は、スタックトレースの非同期(およびその他の多くの不明瞭な要素)を修正する優れたNuGetパッケージがあります: Ben.Demystifier 。他の提案に対するこのパッケージの利点は、スローするコードまたはアセンブリを変更する必要がないことです。キャッチされた例外でDemystifyまたはToStringDemystifiedを呼び出すだけです。

これをコードに適用する:

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<T>.Resultを使用しているため、これはまだ少し複雑です。 GetValueActionメソッドを非同期に変換する場合( async all 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()
9
Douglas

まず最初に、スタックトレースは、ほとんどの人が思っているようには動作しません。これらはデバッグ時に役立ちますが、特にASP.NETでの実行時の使用を目的としていません。

また、スタックトレースは、技術的にはコードが戻る場所であり、コードの由来ではありません。単純な(同期)コードでは、この2つは同じです。コードは常に、呼び出したメソッドに戻ります。ただし、非同期コードでは、これら2つは異なります。繰り返しになりますが、スタックトレースは何が起こるかを示しますnextですが、あなたはpastで何が起こったのか興味があります。

そのため、スタックフレームはニーズに対する正しい答えではありません。 Eric Lippertは、ここでの答えでこれをよく説明しています

@ColeCampbellがリンクしている MSDN記事 は、asyncコードで「偶然の連鎖」(コードが来た場所from)を追跡する1つの方法を説明しています。残念ながら、そのアプローチは限られています(たとえば、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は利用可能です ここ )。その後、次のように使用できます。

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

このアプローチの良い点は、allasyncコードで動作することです:fork/join、awaitables、ConfigureAwait(false)など。欠点は、いくつかのオーバーヘッドを追加します。また、このアプローチは.NET 4.5でのみ機能します。 .NET 4.0の論理呼び出しコンテキストはasync- awareではなく、notは正しく機能します。

更新:NuGetパッケージ(私のブログで説明されています) をリリースしました。これは、PostSharpを使用してプッシュとポップを自動的に挿入します。したがって、良いトレースを取得することは、今ではずっと簡単です。

36
Stephen Cleary

これには、async/awaitキングによるNice nuget拡張があります。

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

からの待機コールを変更する必要があります

Await DownloadAsync(url)

Await DownloadAsync(url).Log()

最後に、catchブロックで、単に呼び出す

ex.StackTraceEx()

重要な注意事項:このメソッドは1回しか呼び出すことができず、ex.StackTraceを事前に評価してはなりません。スタックは1回しか読み取れないようです。

4
Yepeekai