web-dev-qa-db-fra.com

Est-il possible d'obtenir une bonne trace de pile avec les méthodes asynchrones .NET?

J'ai l'exemple de configuration de code suivant dans une application 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!");
}

Malheureusement, lorsque GetValueAction est touché, la trace de pile qui revient est:

    " 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"

Ainsi, je reçois (mutilé) GetValue2 et GetValue dans la trace, mais aucune mention de GetValueAction. Est-ce que je fais quelque chose de mal? Existe-t-il un autre modèle qui me fournira des traces de pile plus complètes?

EDIT: mon objectif n'est pas d'écrire du code en se basant sur la trace de la pile, mais plutôt de faciliter le débogage des échecs dans les méthodes asynchrones.

58
ChaseMedallion

Cette question et sa réponse la plus élevée ont été écrites en 2013. Les choses se sont améliorées depuis lors.

.NET Core 2.1 fournit désormais des traces de pile asynchrone intelligibles dès la sortie de la boîte; voir Améliorations de Stacktrace dans .NET Core 2.1 .

Pour ceux qui sont encore sur .NET Framework, il existe un excellent package NuGet qui corrige l'async (et de nombreuses autres obscurités) dans les traces de pile: Ben.Demystifier . L'avantage de ce package par rapport à d'autres suggestions est qu'il ne nécessite aucune modification du code de lancement ou de l'assembly; vous devez simplement appeler Demystify ou ToStringDemystified sur l'exception interceptée.

Appliquer cela à votre code:

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()<---

Certes, c'est encore un peu compliqué en raison de votre utilisation de Task<T>.Result. Si vous convertissez votre méthode GetValueAction en async (dans l'esprit de async complètement ), vous obtiendrez le résultat net attendu:

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

Tout d'abord, les traces de pile ne font pas ce que la plupart des gens pensent faire. Ils peuvent être utiles lors du débogage, mais ne sont pas destinés à une utilisation à l'exécution, en particulier sur ASP.NET.

En outre, la trace de la pile concerne techniquement où le code revient, pas d'où le code provient. Avec un code simple (synchrone), les deux sont les mêmes: le code retourne toujours à la méthode appelée. Cependant, avec le code asynchrone, ces deux sont différents. Encore une fois, la trace de la pile vous indique ce qui se passera suivant, mais vous êtes intéressé par ce qui s'est passé dans le passé.

Ainsi, le cadre de pile n'est pas la bonne réponse à vos besoins. Eric Lippert l'explique bien dans sa réponse ici .

Le article MSDN auquel @ColeCampbell a lié décrit une façon de suivre les "chaînes de victimes" (d'où vient le code de) avec le code async. Malheureusement, cette approche est limitée (par exemple, elle ne gère pas les scénarios fork/join); cependant, c'est la seule approche que je connaisse qui fonctionne dans les applications du Windows Store.

Puisque vous êtes sur ASP.NET avec le runtime .NET 4.5 complet, vous avez accès à une solution plus puissante pour suivre les chaînes de victimes: le contexte d'appel logique. Vos méthodes async doivent cependant être "activées", donc vous ne les obtenez pas gratuitement comme vous le feriez avec une trace de pile. Je viens d'écrire cela dans un article de blog qui n'est pas encore publié, vous obtenez donc un aperçu. :)

Vous pouvez créer vous-même une "pile" d'appels autour du contexte d'appel logique en tant que tel:

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 est disponible ici ). Vous pouvez ensuite l'utiliser comme ceci:

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

La bonne chose à propos de cette approche est qu'elle fonctionne avec tousasync code: fork/join, attendables personnalisés, ConfigureAwait(false), etc. L'inconvénient est que vous ajoutez des frais généraux. De plus, cette approche ne fonctionne que sur .NET 4.5 ; le contexte d'appel logique sur .NET 4.0 n'est pas compatible avec async et ne fonctionnera pas correctement.

Mise à jour: J'ai publié un package NuGet (décrit sur mon blog) qui utilise PostSharp pour injecter les push et pops automatiquement. Obtenir une bonne trace devrait donc être beaucoup plus simple maintenant.

36
Stephen Cleary

Il y a une extension Nice nuget pour cela par le roi async/wait.

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

vous devez changer votre appel en attente de

Await DownloadAsync(url)

à

Await DownloadAsync(url).Log()

Enfin, dans le bloc catch, appelez simplement

ex.StackTraceEx()

Une remarque importante: cette méthode ne peut être appelée qu'une seule fois et ex.StackTrace ne doit pas être évalué auparavant. Il semble que la pile ne puisse être lue qu'une seule fois.

4
Yepeekai