Partager via


Déboguer une application asynchrone

Ce tutoriel montre comment utiliser la vue Tâches de la fenêtre Stacks parallèles pour déboguer une application asynchrone C#. Cette fenêtre vous permet de comprendre et de vérifier le comportement d’exécution du code qui utilise le modèle asynchrone/await, également appelé modèle asynchrone basé sur les tâches (TAP).

Pour les applications utilisant la bibliothèque parallèle de tâches (TPL), mais pas le modèle asynchrone/await, ou pour les applications C++ à l’aide du runtime d’accès concurrentiel, utilisez la vue Threads dans la fenêtre Stacks parallèles pour le débogage. Pour plus d’informations, consultez Déboguer un interblocage et afficher des threads et des tâches dans la fenêtre Piles parallèles.

La vue Tâches vous aide à :

  • Affichez les visualisations de pile d’appels pour les applications qui utilisent le modèle asynchrone/await. Dans ces scénarios, la vue Tâches fournit une image plus complète de l’état de votre application.

  • Identifiez le code asynchrone qui est planifié pour s’exécuter, mais qui n’est pas encore en cours d’exécution. Par exemple, une requête HTTP qui n’a pas retourné de données est plus susceptible d’apparaître dans la vue Tâches au lieu de l’affichage Threads, ce qui vous permet d’isoler le problème.

  • Aidez à identifier les problèmes tels que le modèle sync-over-async, ainsi que les indicateurs liés aux problèmes potentiels tels que les tâches bloquées ou en attente. Le modèle de code sync-over-async fait référence au code qui appelle des méthodes asynchrones de manière synchrone, qui est connu pour bloquer les threads et est la cause la plus courante de l'épuisement du pool de threads.

Piles d’appels asynchrones

L’affichage Tâches dans Stacks parallèles fournit une visualisation des piles d’appels asynchrones. Vous pouvez donc voir ce qui se passe (ou censé se produire) dans votre application.

Voici quelques points importants à mémoriser lors de l’interprétation des données dans la vue Tâches.

  • Les piles d’appels asynchrones sont des piles d’appels logiques ou virtuelles, et non des piles d’appels physiques représentant la pile. Lorsque vous utilisez du code asynchrone (par exemple, à l’aide du await mot clé), le débogueur fournit une vue des « piles d’appels asynchrones » ou des « piles d’appels virtuels ». Les piles d’appels asynchrones sont différentes des piles d’appels basées sur des threads ou des « piles physiques », car les piles d’appels asynchrones ne sont pas nécessairement en cours d’exécution sur un thread physique. Au lieu de cela, les piles d’appels asynchrones représentent des continuations, ou « promesses », de code qui s’exécuteront ultérieurement de façon asynchrone. Les piles d’appels sont créées à l’aide des continuations.

  • Le code asynchrone planifié, mais qui n’est pas en cours d’exécution n’apparaît pas sur la pile d’appels physiques, mais doit apparaître sur la pile des appels asynchrones dans la vue Tâches. Si vous bloquez des threads à l’aide de méthodes telles que .Wait ou .Result, vous pouvez voir le code dans la pile d’appels physique à la place.

  • Les piles d’appels virtuels asynchrones ne sont pas toujours intuitives, en raison de la ramification qui résulte de l’utilisation d’appels de méthode tels que .WaitAny ou .WaitAll.

  • La fenêtre Pile des appels peut être utile en combinaison avec la vue Tâches, car elle affiche la pile d’appels physique pour le thread en cours d’exécution.

  • Les sections identiques de la pile d’appels virtuels sont regroupées pour simplifier la visualisation des applications complexes.

    L’animation conceptuelle suivante montre comment le regroupement est appliqué aux piles d’appels virtuels. Seuls les segments identiques d’une pile d’appels virtuels sont regroupés. Survolez une pile d’appels groupée pour identifier les threads qui exécutent les tâches.

    Illustration du regroupement des piles d’appels virtuels.

Exemple de code C#

L’exemple de code de cette procédure pas à pas est destiné à une application qui simule un jour dans la vie d’un gorille. L’objectif de l’exercice est de comprendre comment utiliser la vue Tâches de la fenêtre Stacks parallèles pour déboguer une application asynchrone.

L’exemple inclut un exemple d’utilisation de l’antipattern sync-over-async, qui peut entraîner un épuisement du pool de threads.

Pour rendre la pile des appels intuitive, l’exemple d’application effectue les étapes séquentielles suivantes :

  1. Crée un objet représentant un gorille.
  2. Gorille se réveille.
  3. Le gorille fait une promenade matinale.
  4. Gorille trouve des bananes dans la jungle.
  5. Gorille mange.
  6. Gorilla se livre à des manigances.

Créer l’exemple de projet

  1. Ouvrez Visual Studio et créez un projet.

    Si la fenêtre de démarrage n’est pas ouverte, choisissez Fichier>fenêtre de démarrage.

    Dans la fenêtre de démarrage, choisissez Nouveau projet.

    Dans la fenêtre Créer un projet , entrez ou tapez la console dans la zone de recherche. Ensuite, choisissez C# dans la liste de langues, puis choisissez Windows dans la liste plateforme.

    Après avoir appliqué les filtres de langue et de plateforme, choisissez l’application console pour .NET, puis choisissez Suivant.

    Remarque

    Si vous ne voyez pas le modèle approprié, accédez à ToolsGet Tools >and Features..., qui ouvre Visual Studio Installer. Choisissez la charge de travail Développement .NET Desktop, puis choisissez Modifier.

    Dans la fenêtre Configurer votre nouveau projet , tapez un nom ou utilisez le nom par défaut dans la zone Nom du projet . Choisissez ensuite Suivant.

    Pour .NET, choisissez le framework cible recommandé ou .NET 8, puis choisissez Créer.

    Un nouveau projet console s'affiche. Une fois le projet créé, un fichier source s’affiche.

  2. Ouvrez le fichier de code .cs dans le projet. Supprimez son contenu pour créer un fichier de code vide.

  3. Collez le code suivant pour votre langue choisie dans le fichier de code vide.

    using System.Diagnostics;
    
    namespace AsyncTasks_SyncOverAsync
    {
         class Jungle
         {
             public static async Task<int> FindBananas()
             {
                 await Task.Delay(1000);
                 Console.WriteLine("Got bananas.");
                 return 0;
             }
    
             static async Task Gorilla_Start()
             {
                 Debugger.Break();
                 Gorilla koko = new Gorilla();
                 int result = await Task.Run(koko.WakeUp);
             }
    
             static async Task Main(string[] args)
             {
                 List<Task> tasks = new List<Task>();
                 for (int i = 0; i < 2; i++)
                 {
                     Task task = Gorilla_Start();
                     tasks.Add(task);
    
                 }
                 await Task.WhenAll(tasks);
    
             }
         }
    
         class Gorilla
         {
    
             public async Task<int> WakeUp()
             {
                 int myResult = await MorningWalk();
    
                 return myResult;
             }
    
             public async Task<int> MorningWalk()
             {
                 int myResult = await Jungle.FindBananas();
                 GobbleUpBananas(myResult);
    
                 return myResult;
             }
    
             /// <summary>
             /// Calls a .Wait.
             /// </summary>
             public void GobbleUpBananas(int food)
             {
                 Console.WriteLine("Trying to gobble up food synchronously...");
    
                 Task mb = DoSomeMonkeyBusiness();
                 mb.Wait();
    
             }
    
             public async Task DoSomeMonkeyBusiness()
             {
                 Debugger.Break();
                 while (!System.Diagnostics.Debugger.IsAttached)
                 {
                     Thread.Sleep(100);
                 }
    
                 await Task.Delay(30000);
                 Console.WriteLine("Monkey business done");
             }
         }
    }
    

    Après avoir mis à jour le fichier de code, enregistrez vos modifications et générez la solution.

  4. Dans le menu Fichier, sélectionnez Enregistrer tout.

  5. Dans le menu Générer, sélectionnez Générer la solution.

Utilisez la vue Tâches de la fenêtre Piles parallèles

  1. Dans le menu Débogage , sélectionnez Démarrer le débogage (ou F5) et attendez que le premier Debugger.Break() soit atteint.

  2. Appuyez une fois sur F5 et le débogueur s’interrompt à nouveau sur la même Debugger.Break() ligne.

    Cela s’interrompt dans le deuxième appel à Gorilla_Start, qui se produit au sein d’une deuxième tâche asynchrone.

  3. Sélectionnez Déboguer > Fenêtres > Piles parallèles pour ouvrir la fenêtre Piles parallèles, puis sélectionnez Tâches dans la liste déroulante Vue dans la fenêtre.

    Capture d’écran de la vue Tâches dans la fenêtre Piles parallèles.

    Notez que les étiquettes des piles d’appels asynchrones décrivent 2 piles logiques asynchrones. Lorsque vous avez appuyé sur F5 pour la dernière fois, vous avez démarré une autre tâche. Pour simplifier les applications complexes, les piles d’appels asynchrones identiques sont regroupées en une représentation visuelle unique. Cela fournit des informations plus complètes, en particulier dans les scénarios avec de nombreuses tâches.

    Contrairement à la vue Tâches, la fenêtre Pile des appels affiche la pile des appels pour le thread actuel uniquement, et non pour plusieurs tâches. Il est souvent utile de les afficher ensemble pour obtenir une image plus complète de l’état de l’application.

    Capture d’écran de la pile des appels.

    Conseil / Astuce

    La fenêtre Pile des appels peut afficher des informations, par exemple un interblocage, en utilisant la description Async cycle.

    Pendant le débogage, vous pouvez choisir d’afficher ou de masquer le code externe. Pour désactiver la fonctionnalité, cliquez avec le bouton droit sur l’en-tête de table Name de la fenêtre Pile des appels, puis sélectionnez ou désactivez Afficher le code externe. Si vous affichez du code externe, vous pouvez toujours utiliser cette procédure pas à pas, mais vos résultats peuvent différer des illustrations.

  4. Appuyez à nouveau sur F5 , puis le débogueur s’interrompt dans la DoSomeMonkeyBusiness méthode.

    Capture d’écran de la vue Tâches après F5.

    Cette vue montre une pile d’appels asynchrone plus complète après l’ajout de méthodes asynchrones à la chaîne de continuation interne, qui se produit lors de l’utilisation await et de méthodes similaires. DoSomeMonkeyBusiness peut ou non être présent en haut de la pile des appels asynchrones, car il s’agit d’une méthode asynchrone, mais n’a pas encore été ajoutée à la chaîne de continuation. Nous allons explorer pourquoi c’est le cas dans les étapes qui suivent.

    Cette vue affiche également l’icône bloquée pour Jungle.MainStatus Blocked. Il s’agit d’informations, mais n’indique généralement pas de problème. Une tâche bloquée est une tâche bloquée, car elle attend la fin d’une autre tâche, un événement à signaler ou un verrou à libérer.

  5. Pointez sur la GobbleUpBananas méthode pour obtenir des informations sur les deux threads qui exécutent les tâches.

    Capture d’écran des threads associés à la pile des appels.

    Le thread actuel apparaît également dans la liste Thread de la barre d’outils de débogage.

    Capture d'écran du fil d'exécution actuel dans la barre d'outils de débogage.

    Vous pouvez utiliser la liste des threads pour basculer le contexte du débogueur vers un autre thread.

  6. Appuyez à nouveau sur F5 et le débogueur s’interrompt dans la DoSomeMonkeyBusiness méthode pour la deuxième tâche.

    Capture d’écran de la vue Tâches après la deuxième F5.

    Selon le minutage de l’exécution de la tâche, à ce stade, vous voyez des piles d’appels asynchrones distinctes ou groupées.

    Dans l’illustration précédente, les piles d’appels asynchrones pour les deux tâches sont distinctes, car elles ne sont pas identiques.

  7. Appuyez à nouveau sur F5 et vous verrez un long délai se produire et la vue Tâches n’affiche aucune information de pile d’appels asynchrones.

    Le délai est dû à une tâche longue. À des fins de cet exemple, il simule une tâche de longue durée telle qu’une requête web, ce qui peut entraîner une insuffisance de pool de threads. Rien n’apparaît dans l’affichage Tâches, car même si les tâches peuvent être bloquées, vous n’êtes pas actuellement suspendu dans le débogueur.

    Conseil / Astuce

    Le bouton Arrêter tout est un bon moyen d’obtenir des informations sur la pile des appels si un blocage se produit ou que toutes les tâches et threads sont actuellement bloqués.

  8. En haut de l’IDE dans la barre d’outils Débogage, sélectionnez le bouton Arrêter tout (icône pause), Ctrl + Alt + Arrêt.

    Capture d’écran de la vue Tâches après avoir sélectionné Break All.

    Près du haut de la pile d’appels asynchrones dans la vue Tâches, vous voyez que GobbleUpBananas est bloqué. En fait, deux tâches sont bloquées au même point. Une tâche bloquée n’est pas nécessairement inattendue et ne signifie pas nécessairement qu’il y a un problème. Toutefois, le délai observé dans l’exécution indique un problème et les informations de pile des appels ici indiquent l’emplacement du problème.

    Dans le côté gauche de la capture d’écran précédente, la flèche verte bouclée indique le contexte actuel du débogueur. Les deux tâches sont bloquées sur mb.Wait() dans la méthode GobbleUpBananas.

    La fenêtre Pile des appels indique aussi que le thread actuel est bloqué.

    Capture d’écran de la pile des appels après avoir sélectionné Arrêter tout.

    L'appel à Wait() bloque les threads lors de l'appel synchrone à GobbleUpBananas. Il s’agit d’un exemple d’antipattern « sync-over-async » (synchronisation sur asynchronisation). Si cela se produisait sur un thread de l’interface utilisateur ou sous des charges de traitement importantes, cela serait généralement résolu par une correction du code à l’aide de await. Pour en savoir plus, consultez Débogage de la saturation du pool de threads. Pour utiliser des outils de profilage afin de déboguer l'épuisement du pool de threads, consultez l’étude de cas : Isoler un problème de performance.

    Il est également intéressant de noter que DoSomeMonkeyBusiness n’apparaît pas dans la pile des appels. Il est actuellement planifié, pas en cours d’exécution, de sorte qu’il apparaît uniquement dans la pile des appels asynchrones dans l’affichage Tâches.

    Conseil / Astuce

    Le débogueur interrompt l'exécution du code de manière individuelle pour chaque thread. Par exemple, cela signifie que si vous appuyez sur F5 pour poursuivre l’exécution et que l’application atteint le prochain point d’arrêt, elle peut passer dans le code d'un autre thread d'exécution. Si vous devez gérer cela à des fins de débogage, vous pouvez ajouter des points d’arrêt supplémentaires, ajouter des points d’arrêt conditionnels ou utiliser Break All. Pour plus d’informations sur ce comportement, consultez Suivre un thread unique avec des points d’arrêt conditionnels.

Corriger l’exemple de code

  1. Remplacez la méthode GobbleUpBananas par le code suivant.

     public async Task GobbleUpBananas(int food) // Previously returned void.
     {
         Console.WriteLine("Trying to gobble up food...");
    
         //Task mb = DoSomeMonkeyBusiness();
         //mb.Wait();
         await DoSomeMonkeyBusiness();
     }
    
  2. Dans la MorningWalk méthode, appelez GobbleUpBananas en utilisant await.

    await GobbleUpBananas(myResult);
    
  3. Sélectionnez le bouton Redémarrer (Ctrl + Maj + F5), puis appuyez plusieurs fois sur F5 jusqu’à ce que l’application apparaisse « bloquer ».

  4. Appuyez sur Arrêter tout.

    Cette fois, GobbleUpBananas s’exécute de manière asynchrone. Quand vous faites une pause, vous voyez la pile d'appels asynchrones.

    Capture d’écran du contexte du débogueur après le correctif de code.

    La fenêtre Pile des appels est vide, à l’exception de l’entrée ExternalCode.

    L’éditeur de code ne nous montre rien, sauf qu’il fournit un message indiquant que tous les threads exécutent du code externe.

    Toutefois, la vue Tâches fournit des informations utiles. DoSomeMonkeyBusiness est en haut de la pile des appels asynchrones, comme prévu. Cela nous indique correctement où se trouve la méthode longue exécution. Cela est utile pour isoler les problèmes async/await lorsque la pile d’appels physique dans la fenêtre Pile d’appels ne fournit pas suffisamment de détails.

Résumé

Cette procédure pas à pas a présenté la fenêtre du débogueur Piles parallèles. Utilisez cette fenêtre sur les applications qui utilisent le modèle asynchrone/await.