Freigeben über


Debuggen einer asynchronen Anwendung

In diesem Lernprogramm wird gezeigt, wie Sie die Aufgabenansicht des Fensters "Parallele Stapel" verwenden, um eine Asynchrone C#-Anwendung zu debuggen. Dieses Fenster hilft Ihnen, das Laufzeitverhalten von Code zu verstehen und zu überprüfen, der das asynchrone/await-Muster verwendet, auch als aufgabenbasiertes asynchrones Muster (TAP) bezeichnet.

Verwenden Sie für Apps, die die Task Parallel Library (TPL) verwenden, jedoch nicht das asynchrone/await-Muster oder für C++-Apps, die die Parallelitätslaufzeit verwenden, die Threads-Ansicht im Fenster "Parallele Stapel" zum Debuggen. Weitere Informationen finden Sie unter Debuggen eines Deadlocks und Anzeigen von Threads und Aufgaben im Fenster "Parallele Stapel".

Die Ansicht "Aufgaben" hilft Ihnen bei folgenden Aufgaben:

  • Zeigen Sie Aufrufstapelvisualisierungen für Apps an, die das asynchrone/await-Muster verwenden. In diesen Szenarien bietet die Ansicht "Aufgaben" ein vollständiges Bild des App-Zustands.

  • Identifizieren Sie asynchronen Code, der für die Ausführung geplant ist, aber noch nicht ausgeführt wird. Beispielsweise wird eine HTTP-Anforderung, die keine Daten zurückgegeben hat, in der Aufgabenansicht anstelle der Threads-Ansicht angezeigt, wodurch Sie das Problem isolieren können.

  • Helfen Sie beim Identifizieren von Problemen wie dem Sync-over-Async-Pattern sowie von Hinweisen auf potenzielle Probleme wie blockierte oder wartende Aufgaben. Das Sync-over-Async-Codemuster bezieht sich auf Code, der asynchrone Methoden auf synchrone Weise aufruft, was dazu bekannt ist, Threads zu blockieren und die häufigste Ursache für das Verhungern des Thread-Pools darstellt.

Asynchrone Aufrufstapel

Die Aufgabenansicht in parallelen Stapeln bietet eine Visualisierung für asynchrone Aufrufstapel, sodass Sie sehen können, was in Ihrer Anwendung passiert (oder was passieren soll).

Hier sind einige wichtige Punkte, die Sie beim Interpretieren von Daten in der Ansicht "Aufgaben" beachten sollten.

  • Asynchrone Aufrufstapel sind logische oder virtuelle Aufrufstapel und keine physischen Aufrufstapel, die den Stapel darstellen. Beim Arbeiten mit asynchronem Code (z. B. mithilfe des await Schlüsselworts) stellt der Debugger eine Ansicht der "asynchronen Aufrufstapel" oder "virtuelle Aufrufstapel" bereit. Asynchrone Aufrufstapel unterscheiden sich von threadbasierten Aufrufstapeln oder "physischen Stapeln", da asynchrone Aufrufstapel derzeit nicht unbedingt in einem physischen Thread ausgeführt werden. Stattdessen handelt es sich bei den asynchronen Aufrufstapeln um Fortsetzungen oder "Zusagen" von Code, die in Zukunft asynchron ausgeführt werden. Die Aufrufstapel werden mithilfe von Fortsetzungen erstellt.

  • Asynchroner Code, der geplant, aber derzeit nicht ausgeführt wird, wird nicht im physischen Aufrufstapel angezeigt, sollte aber im asynchronen Aufrufstapel in der Aufgabenansicht angezeigt werden. Wenn Sie Threads mit Methoden wie .Wait oder .Resultblockieren, wird stattdessen der Code im physischen Aufrufstapel angezeigt.

  • Asynchrone virtuelle Aufrufstapel sind nicht immer intuitiv, da sie aufgrund des Verzweigens entstehen, das durch den Einsatz von Methodenaufrufen wie .WaitAny oder .WaitAll verursacht wird.

  • Das Fenster "Aufrufstapel " kann in Kombination mit der Ansicht "Aufgaben" nützlich sein, da der physische Aufrufstapel für den aktuellen ausgeführten Thread angezeigt wird.

  • Identische Abschnitte des virtuellen Aufrufstapels werden gruppiert, um die Visualisierung für komplexe Apps zu vereinfachen.

    Die folgende konzeptionelle Animation zeigt, wie die Gruppierung auf virtuelle Aufrufstapel angewendet wird. Es werden nur identische Segmente eines virtuellen Anrufstapels gruppiert. Fahren Sie mit der Maus über einen gruppierten Aufrufstapel, um die Threads zu identifizieren, die die Aufgaben ausführen.

    Abbildung der Gruppierung virtueller Aufrufstapel.

C#-Beispiel

Der Beispielcode in diesem Tutorial ist für eine Anwendung, die einen Tag im Leben eines Gorillas simuliert. Der Zweck der Übung besteht darin, zu verstehen, wie Sie die Aufgabenansicht des Fensters "Parallele Stapel" verwenden, um eine asynchrone Anwendung zu debuggen.

Das Beispiel enthält ein Beispiel für die Verwendung des Sync-over-Async-Antipatterns, das zu einem Aushungern des Threadpools führen kann.

Um den Aufrufstapel intuitiv zu gestalten, führt die Beispiel-App die folgenden sequenziellen Schritte aus:

  1. Erstellt ein Objekt, das einen Gorilla darstellt.
  2. Gorilla wacht auf.
  3. Gorilla geht morgens zu Fuß.
  4. Gorilla findet Bananen im Dschungel.
  5. Gorilla isst.
  6. Gorilla betreibt Affengeschäfte.

Erstellen des Beispielprojekts

  1. Öffnen Sie Visual Studio, und erstellen Sie ein neues Projekt.

    Wenn das Startfenster nicht geöffnet ist, wählen Sie Datei>Startfensteraus.

    Wählen Sie im Startfenster " Neues Projekt" aus.

    Geben Sie im Fenster Neues Projekt erstellen im Suchfeld Konsole ein. Wählen Sie als Nächstes C#- aus der Liste "Sprache" aus, und wählen Sie dann in der Liste "Plattform" Windows aus.

    Nachdem Sie die Sprach- und Plattformfilter angewendet haben, wählen Sie die Konsolen-App für .NET und dann "Weiter" aus.

    Hinweis

    Wenn die richtige Vorlage nicht angezeigt wird, wechseln Sie zu Tools>»Tools und Features abrufen«, wodurch das Visual Studio-Installationsprogramm geöffnet wird. Wählen Sie beispielsweise die Workload .NET-Desktopentwicklung aus, und klicken Sie anschließend auf Ändern.

    Geben Sie im Fenster " Neues Projekt konfigurieren " einen Namen ein, oder verwenden Sie den Standardnamen im Feld "Projektname ". Klicken Sie dann auf Weiter.

    Wählen Sie für .NET entweder das empfohlene Zielframework oder .NET 8 und dann "Erstellen" aus.

    Ein neues Konsolenprojekt wird angezeigt. Nachdem das Projekt erstellt wurde, wird eine Quelldatei angezeigt.

  2. Öffnen Sie die .cs Codedatei im Projekt. Löschen Sie den Inhalt, um eine leere Codedatei zu erstellen.

  3. Fügen Sie den folgenden Code für die ausgewählte Sprache in die leere Codedatei ein.

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

    Nachdem Sie die Codedatei aktualisiert haben, speichern Sie Die Änderungen, und erstellen Sie die Lösung.

  4. Wählen Sie im Menü "Datei " die Option "Alle speichern" aus.

  5. Wählen Sie im Menü Erstellen die Option Lösung erstellen.

Verwenden Sie die Aufgabenansicht des Fensters "Parallele Stapel"

  1. Wählen Sie im Menü " Debuggen " die Option "Debuggen starten " (oder "F5") aus, und warten Sie, bis der erste Debugger.Break() Treffer erfolgt.

  2. Drücken Sie einmal F5 , und der Debugger wird wieder in derselben Debugger.Break() Zeile angehalten.

    Dies pausiert im zweiten Aufruf des Gorilla_Startdie innerhalb einer zweiten asynchronen Aufgabe stattfindet.

  3. Wählen Sie Debug > Windows > Parallel-Stapel aus, um das Fenster "Parallele Stapel" zu öffnen, und wählen Sie dann Aufgaben aus der Dropdown-Liste Ansicht im Fenster aus.

    Screenshot der Ansicht

    Beachten Sie, dass die Bezeichnungen für die asynchronen Aufrufstapel 2 Asynchrone logische Stapel beschreiben. Wenn Sie zuletzt F5 gedrückt haben, haben Sie eine weitere Aufgabe gestartet. Zur Vereinfachung in komplexen Apps werden identische asynchrone Aufrufstapel in einer einzigen visuellen Darstellung gruppiert. Dies bietet umfassendere Informationen, insbesondere in Szenarien mit vielen Aufgaben.

    Im Gegensatz zur Ansicht "Aufgaben" zeigt das Fenster "Aufrufstapel " den Aufrufstapel nur für den aktuellen Thread an, nicht für mehrere Aufgaben. Es ist häufig hilfreich, beide zusammen anzuzeigen, um ein vollständiges Bild des App-Zustands anzuzeigen.

    Screenshot des Anrufstapels.

    Tipp

    Das Stapel aufrufen-Fenster kann Ihnen Informationen wie z. B. einen Deadlock anzeigen, indem Sie die Beschreibung Async cycle.

    Während des Debuggens können Sie umschalten, ob externer Code angezeigt wird. Wenn Sie das Feature umschalten möchten, klicken Sie mit der rechten Maustaste auf die Kopfzeile der Namenstabelle des Fensters "Anrufliste ", und aktivieren oder deaktivieren Sie dann " Externer Code anzeigen". Wenn Sie externen Code anzeigen, können Sie diese exemplarische Vorgehensweise weiterhin verwenden, ihre Ergebnisse können sich jedoch von den Abbildungen unterscheiden.

  4. Drücken Sie erneut F5 , und der Debugger wird in der DoSomeMonkeyBusiness Methode angehalten.

    Screenshot der Ansicht

    Diese Ansicht zeigt einen vollständigen asynchronen Aufrufstapel, nachdem der internen Fortsetzungskette weitere asynchrone Methoden hinzugefügt wurden, was passiert, wenn await und ähnliche Methoden verwendet werden. DoSomeMonkeyBusiness ist möglicherweise oder möglicherweise nicht oben im asynchronen Aufrufstapel vorhanden, da es sich um eine asynchrone Methode handelt, die jedoch noch nicht zur Fortsetzungskette hinzugefügt wurde. Wir werden untersuchen, warum dies in den folgenden Schritten der Fall ist.

    In dieser Ansicht wird auch das Symbol "Blockiert" für Jungle.Main. Dies ist informativ, weist jedoch in der Regel nicht auf ein Problem hin. Eine blockierte Aufgabe ist eine, die blockiert wird, weil sie auf eine andere Aufgabe wartet, bis sie abgeschlossen ist, ein Ereignis, das signalisiert wird oder eine Sperre losgelassen wird.

  5. Zeigen Sie mit der Maus auf die GobbleUpBananas Methode, um Informationen zu den beiden Threads abzurufen, die die Aufgaben ausführen.

    Screenshot der Threads, die dem Aufrufstapel zugeordnet sind.

    Der aktuelle Thread wird auch in der Threadliste in der Debugsymbolleiste angezeigt.

    Screenshot des aktuellen Threads in der Debugsymbolleiste.

    Sie können die Threadliste verwenden, um den Debuggerkontext in einen anderen Thread zu wechseln.

  6. Drücken Sie erneut F5 , und der Debugger hält in der DoSomeMonkeyBusiness Methode für den zweiten Vorgang an.

    Screenshot der Ansicht

    Je nach Zeitpunkt der Aufgabenausführung werden an diesem Punkt entweder separate oder gruppierte asynchrone Aufrufstapel angezeigt.

    In der vorherigen Abbildung sind die asynchronen Aufrufstapel für die beiden Aufgaben getrennt, da sie nicht identisch sind.

  7. Drücken Sie erneut F5 , und es wird eine lange Verzögerung angezeigt, und in der Aufgabenansicht werden keine asynchronen Aufrufstapelinformationen angezeigt.

    Die Verzögerung wird durch eine lang andauernde Aufgabe verursacht. In diesem Beispiel wird eine lang andauernde Aufgabe wie eine Webanforderung simuliert, die zu einem Threadpool-Verhungern führen könnte. In der Aufgabenansicht wird nichts angezeigt, denn obwohl Aufgaben blockiert sein können, sind Sie im Debugger derzeit nicht angehalten.

    Tipp

    Die Alles abbrechen Die Schaltfläche Aufrufen ist eine gute Möglichkeit, um Informationen über den Aufrufstapel zu erhalten, wenn eine Blockierung auftritt oder alle Aufgaben und Threads derzeit blockiert sind.

  8. Wählen Sie oben in der IDE in der Fehlersuche-Symbolleiste die Option Alles abbrechen Schaltfläche (Pausensymbol), Strg + Alt + Pause.

    Screenshot der Ansicht

    Am oberen Rand des asynchronen Aufrufstapels in der Aufgabenansicht sehen Sie, dass GobbleUpBananas blockiert ist. Tatsächlich werden zwei Vorgänge an demselben Punkt blockiert. Eine blockierte Aufgabe ist nicht unbedingt unerwartet und bedeutet nicht unbedingt, dass ein Problem vorliegt. Die beobachtete Verzögerung bei der Ausführung weist jedoch auf ein Problem hin, und die Informationen zum Aufrufstapel hier zeigen den Speicherort des Problems an.

    Auf der linken Seite des vorherigen Screenshots zeigt der geschweifte grüne Pfeil den aktuellen Debuggerkontext an. Die beiden Aufgaben sind blockiert auf mb.Wait() in den GobbleUpBananas Methode.

    Das Fenster "Aufrufstapel" zeigt auch an, dass der aktuelle Thread blockiert ist.

    Screenshot des Anrufstapels nach dem Auswählen von

    Der Aufruf von Wait() blockiert die Threads innerhalb des synchronen Aufrufs von GobbleUpBananas. Dies ist ein Beispiel für das Sync-über-Async-Antipattern, und wenn dies in einem UI-Thread oder unter hoher Verarbeitungsbelastung aufgetreten ist, würde es in der Regel mit einer Codekorrektur await behoben werden. Für weitere Informationen siehe Fehlersuche thread pool starvation. Um Werkzeuge zur Profilerstellung zu verwenden, um Thread-Pool-Starvation zu beheben, siehe Fallstudie: Isolieren eines Leistungsproblems.

    Auch interessant, DoSomeMonkeyBusiness wird nicht auf dem Anrufstapel angezeigt. Die Transaktion ist derzeit geplant und nicht in Ausführung, so dass sie nur im asynchronen Aufrufstapel in der Aufgabenansicht angezeigt wird.

    Tipp

    Der Debugger greift auf per-Thread-Ebene in den Code ein. Das bedeutet zum Beispiel, dass wenn Sie F5 um die Ausführung fortzusetzen, und die Anwendung den nächsten Haltepunkt erreicht, kann sie in den Code eines anderen Threads einbrechen. Wenn Sie dies für Debuggingzwecke verwalten müssen, können Sie zusätzliche Haltepunkte hinzufügen, bedingte Haltepunkte hinzufügen oder "Alle aufheben" verwenden. Weitere Informationen zu diesem Verhalten finden Sie unter Einen einzelnen Thread mit bedingten Haltepunkten nachverfolgen.

Reparieren Sie den Beispielcode

  1. Ersetzen Sie die GobbleUpBananas-Methode durch den folgenden Code.

     public async Task GobbleUpBananas(int food) // Previously returned void.
     {
         Console.WriteLine("Trying to gobble up food...");
    
         //Task mb = DoSomeMonkeyBusiness();
         //mb.Wait();
         await DoSomeMonkeyBusiness();
     }
    
  2. Rufen Sie in der MorningWalk Methode GobbleUpBananas mithilfe von await auf.

    await GobbleUpBananas(myResult);
    
  3. Wählen Sie die Schaltfläche " Neustart " aus (STRG+UMSCHALT+F5), und drücken Sie dann mehrmals F5, bis die App "hängen" angezeigt wird.

  4. Drücken Sie Alles unterbrechen.

    Dieses Mal GobbleUpBananas wird asynchron ausgeführt. Beim Unterbrechen wird der asynchrone Aufrufstapel angezeigt.

    Screenshot des Debuggerkontexts nach der Codekorrektur.

    Das Fenster "Anrufstapel" ist leer, mit Ausnahme des ExternalCode Eintrags.

    Der Code-Editor zeigt uns nichts an, es sei denn, er gibt eine Meldung an, die angibt, dass alle Threads externen Code ausführen.

    Die Ansicht "Aufgaben" bietet jedoch nützliche Informationen. DoSomeMonkeyBusiness befindet sich oben im asynchronen Aufrufstapel wie erwartet. Dadurch wird uns korrekt mitgeteilt, wo sich die langlaufende Methode befindet. Dies ist hilfreich, um Async/Wait-Probleme zu isolieren, wenn der physische Aufrufstapel im Aufrufstapel-Fenster nicht genügend Einzelheiten liefert.

Zusammenfassung

In dieser Schritt-für-Schritt-Anleitung wurde das Parallel-Stacks-Debuggerfenster veranschaulicht. Verwenden Sie dieses Fenster für Apps, die das asynchrone/await-Muster verwenden.