Condividi tramite


Eseguire il debug di un'applicazione asincrona

Questa esercitazione illustra come usare la visualizzazione attività della finestra Stack paralleli per eseguire il debug di un'applicazione asincrona C#. Questa finestra consente di comprendere e verificare il comportamento di runtime del codice che usa il modello asincrono/await, detto anche modello asincrono basato su attività (TAP).

Per le app che usano la libreria TPL (Task Parallel Library), ma non il modello async/await o per le app C++ che usano il runtime di concorrenza, usare la visualizzazione Thread nella finestra Stack paralleli per il debug. Per altre informazioni, vedere Eseguire il debug di un deadlock e visualizzare thread e attività nella finestra Stack paralleli.

La visualizzazione Attività consente di:

  • Visualizzare le visualizzazioni dello stack di chiamate per le app che usano il modello async/await. In questi scenari, la visualizzazione Attività offre un quadro più completo dello stato dell'app.

  • Identificare il codice asincrono pianificato per l'esecuzione ma che non è ancora in esecuzione. Ad esempio, una richiesta HTTP che non ha restituito dati è più probabile che venga visualizzata nella visualizzazione Attività anziché nella visualizzazione Thread, che consente di isolare il problema.

  • Aiuta a identificare problemi come il modello di sincronizzazione su asincrono, insieme alle indicazioni relative a potenziali problemi, come attività bloccate o in attesa. Il modello di codice sync-over-async si riferisce al codice che chiama metodi asincroni in modo sincrono, che è noto per bloccare i thread ed è la causa più comune della fame del pool di thread.

Stack di chiamate asincrone

La vista delle Attività in Stack Paralleli fornisce una rappresentazione degli stack di chiamate asincrone, in modo da poter vedere cosa accade (o dovrebbe accadere) nella tua applicazione.

Ecco alcuni punti importanti da ricordare durante l'interpretazione dei dati nella visualizzazione Attività.

  • Gli stack di chiamate asincroni sono stack di chiamate logiche o virtuali, non stack di chiamate fisiche che rappresentano lo stack. Quando si usa il codice asincrono ,ad esempio usando la await parola chiave , il debugger fornisce una visualizzazione degli "stack di chiamate asincroni" o "stack di chiamate virtuali". Gli stack di chiamate asincroni sono diversi dagli stack di chiamate basati su thread o "stack fisici", perché gli stack di chiamate asincroni non sono necessariamente in esecuzione in alcun thread fisico. Gli stack di chiamate asincroni sono invece continuazioni o "promesse" di codice che verranno eseguiti in futuro, in modo asincrono. Gli stack di chiamate vengono creati usando le continuazioni.

  • Il codice asincrono pianificato ma non attualmente in esecuzione non viene visualizzato nello stack di chiamate fisiche, ma dovrebbe essere visualizzato nello stack di chiamate asincrono nella visualizzazione Attività. Se si bloccano i thread usando metodi come .Wait o .Result, è possibile che il codice venga visualizzato nello stack di chiamate fisiche.

  • Gli stack di chiamate virtuali asincroni non sono sempre intuitivi, a causa della diramazione risultante dall'uso di chiamate di metodo come .WaitAny o .WaitAll.

  • La finestra Stack di chiamate può essere utile in combinazione con la visualizzazione Attività, perché mostra lo stack di chiamate fisiche per il thread in esecuzione corrente.

  • Le sezioni identiche dello stack di chiamate virtuali vengono raggruppate per semplificare la visualizzazione per app complesse.

    L'animazione concettuale seguente illustra come il raggruppamento viene applicato agli stack di chiamate virtuali. Vengono raggruppati solo segmenti identici di uno stack di chiamate virtuali. Passare il puntatore del mouse su uno stack di chiamate raggruppate per indefy i thread che eseguono le attività.

    Illustrazione del raggruppamento di stack di chiamate virtuali.

Esempio in C#

Il codice di esempio in questa procedura dettagliata è per un'applicazione che simula un giorno nella vita di un gorilla. Lo scopo dell'esercizio è comprendere come usare la visualizzazione delle attività della finestra Stack paralleli per eseguire il debug di un'applicazione asincrona.

L'esempio include un esempio dell'utilizzo dell'antipattern sincrono-su-asincrono, che può comportare l'esaurimento del pool di thread.

Per rendere intuitivo lo stack di chiamate, l'app di esempio esegue i passaggi sequenziali seguenti:

  1. Crea un oggetto che rappresenta un gorilla.
  2. Gorilla si sveglia.
  3. Gorilla va in una passeggiata di mattina.
  4. Gorilla trova banane nella giungla.
  5. Gorilla mangia.
  6. Gorilla si impegna in attività da furfante.

Creare il progetto di esempio

  1. Aprire Visual Studio e creare un nuovo progetto.

    Se la finestra iniziale non è aperta, scegliere Finestradi avvio>.

    Nella finestra iniziale scegliere Nuovo progetto.

    Nella finestra Crea un nuovo progetto immettere o digitare console nella casella di ricerca. Scegliere quindi C# dall'elenco Linguaggio e quindi scegliere Windows dall'elenco Piattaforma.

    Dopo aver applicato i filtri di linguaggio e piattaforma, scegliere l'app console per .NET e quindi scegliere Avanti.

    Annotazioni

    Se non vedi il modello corretto, vai a Strumenti>Recupera strumenti e funzionalità... Verrà aperto il programma di installazione di Visual Studio. Scegliere il carico di lavoro Sviluppo di applicazioni desktop .NET, quindi scegliere Modifica.

    Nella finestra Configura il nuovo progetto digitare un nome o usare il nome predefinito nella casella Nome progetto . Scegliere quindi Avanti.

    Per .NET scegliere il framework di destinazione consigliato o .NET 8 e quindi scegliere Crea.

    Appare un nuovo progetto console. Dopo aver creato il progetto, viene visualizzato un file di origine.

  2. Aprire il file di codice .cs nel progetto. Eliminare il contenuto per creare un file di codice vuoto.

  3. Incollare il codice seguente per la lingua scelta nel file di codice vuoto.

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

    Dopo aver aggiornato il file di codice, salvare le modifiche e compilare la soluzione.

  4. Scegliere Salva tutto dal menu File.

  5. Scegliere Compila soluzione dal menu Compila.

Usare la visualizzazione delle attività della finestra dello Stack parallelo

  1. Nel menu Debug selezionare Avvia debug (o F5) e attendere che venga raggiunto il primo Debugger.Break() .

  2. Premere F5 una volta e il debugger si sospende nuovamente sulla stessa Debugger.Break() riga.

    Questa operazione viene sospesa nella seconda chiamata a Gorilla_Start, che si verifica all'interno di una seconda attività asincrona.

  3. Selezionare Debug > stack paralleli di Windows > per aprire la finestra Stack paralleli e quindi selezionare Attività dall'elenco a discesa Visualizza nella finestra.

    Screenshot della visualizzazione Attività nella finestra Stack Parallel.

    Si noti che le etichette per gli stack di chiamate asincrone descrivono 2 stack logici asincroni. Quando si preme F5 per l'ultima volta, è stata avviata un'altra attività. Per semplificare le app complesse, gli stack di chiamate asincroni identici vengono raggruppati in una singola rappresentazione visiva. In questo modo vengono fornite informazioni più complete, in particolare negli scenari con molte attività.

    A differenza della visualizzazione Attività, nella finestra Stack di chiamate viene visualizzato lo stack di chiamate solo per il thread corrente, non per più attività. Spesso è utile visualizzarli entrambi insieme per un quadro più completo dello stato dell'app.

    Screenshot dello stack di chiamate.

    Suggerimento

    La finestra Call Stack può visualizzare informazioni tra cui un deadlock, usando la descrizione Async cycle.

    Durante il debug, è possibile attivare o disattivare la visualizzazione di codice esterno. Per attivare o disattivare la funzionalità, fare clic con il pulsante destro del mouse sull'intestazione della tabella Nome della finestra Stack di chiamate e quindi selezionare o deselezionare Mostra codice esterno. Se si visualizza codice esterno, è comunque possibile usare questa procedura dettagliata, ma i risultati potrebbero differire dalle illustrazioni.

  4. Premere di nuovo F5 e il debugger viene sospeso nel metodo DoSomeMonkeyBusiness.

    Screenshot della visualizzazione Attività dopo F5.

    Questa vista mostra uno stack di chiamate asincrono più completo dopo l'aggiunta di altri metodi asincroni alla catena di continuazione interna, che si verifica quando si usano await e metodi simili. DoSomeMonkeyBusiness può o non essere presente all'inizio dello stack di chiamate asincrone perché è un metodo asincrono ma non è ancora stato aggiunto alla catena di continuazione. Si esaminerà il motivo per cui questo è il caso nei passaggi che seguono.

    Questa visualizzazione mostra anche l'icona bloccata per Jungle.MainStato bloccato. Questo è informativo, ma in genere non indica un problema. Un'attività bloccata è un'attività bloccata perché è in attesa del completamento di un'altra attività, un evento da segnalare o un blocco da rilasciare.

  5. Passare il puntatore del mouse sul GobbleUpBananas metodo per ottenere informazioni sui due thread che eseguono le attività.

    Screenshot dei thread associati allo stack di chiamate.

    Il thread corrente viene visualizzato anche nell'elenco Thread sulla barra degli strumenti Debug.

    Screenshot del thread corrente nella barra degli strumenti di debug.

    È possibile usare l'elenco Thread per cambiare il contesto del debugger in un thread diverso.

  6. Premere di nuovo F5 e il debugger si ferma nel metodo DoSomeMonkeyBusiness per la seconda attività.

    Screenshot della visualizzazione Attività dopo il secondo F5.

    A seconda dell'intervallo di esecuzione dell'attività, a questo punto vengono visualizzati stack di chiamate asincroni separati o raggruppati.

    Nella figura precedente gli stack di chiamate asincroni per le due attività sono separati perché non sono identici.

  7. Premere di nuovo F5 e si noterà un lungo ritardo e la visualizzazione Attività non mostra informazioni sullo stack di chiamate asincrone.

    Il ritardo è causato da un'attività a esecuzione prolungata. Ai fini di questo esempio, simula un'attività a esecuzione prolungata, come una richiesta web, che potrebbe causare l'esaurimento del pool di thread. Non viene visualizzato alcun elemento nella visualizzazione Attività perché anche se le attività potrebbero essere bloccate non sono attualmente sospese nel debugger.

    Suggerimento

    Il pulsante Interrompi tutto è un buon modo per ottenere informazioni sullo stack di chiamate se si verifica un deadlock o tutte le attività e i thread sono attualmente bloccati.

  8. Nella parte superiore dell'IDE nella barra degli strumenti Debug selezionare il pulsante Interrompi tutto (icona pausa), CTRL + ALT + INTERR.

    Screenshot della visualizzazione Attività dopo la selezione di Interrompi tutto.

    Nella parte superiore dello stack di chiamate asincrone nella visualizzazione Attività si noterà che GobbleUpBananas è bloccato. Infatti, due attività vengono bloccate allo stesso punto. Un'attività bloccata non è necessariamente imprevista e non significa necessariamente che si verifichi un problema. Tuttavia, il ritardo osservato nell'esecuzione indica un problema e le informazioni sullo stack di chiamate qui mostrano la posizione del problema.

    Nel lato sinistro dello screenshot precedente, la freccia verde arricciata indica il contesto del debugger corrente. Le due attività vengono bloccate nel metodo mb.Wait() su GobbleUpBananas.

    La finestra Stack di chiamate mostra anche che il thread corrente è bloccato.

    Screenshot dello stack di chiamate dopo aver selezionato Interrompi tutto.

    La chiamata a Wait() blocca i thread all'interno della chiamata sincrona a GobbleUpBananas. Questo è un esempio dell'antipattern di sincronizzazione sopra asincronia e, se questo avviene su un thread UI o sotto carichi di lavoro elevati di elaborazione, in genere viene risolto con una correzione nel codice usando await. Per altre informazioni, vedere Debug dell'esaurimento del pool di thread. Per utilizzare gli strumenti di profilatura per il debug dell'esaurimento del pool di thread, consultare Studio di caso: Isolare un problema di prestazioni.

    Inoltre, DoSomeMonkeyBusiness non viene visualizzato nello stack di chiamate. È attualmente pianificato, non in esecuzione, quindi viene visualizzato solo nello stack di chiamate asincrone nella vista Attività.

    Suggerimento

    Il debugger suddivide il codice in base al thread. Questo significa, ad esempio, che se si preme F5 per continuare l'esecuzione e l'app raggiunge il punto di interruzione successivo, potrebbe entrare nel codice in un thread diverso. Se è necessario gestirlo a scopo di debug, è possibile aggiungere punti di interruzione aggiuntivi, aggiungere punti di interruzione condizionali o usare Interrompi tutto. Per altre informazioni su questo comportamento, vedere Seguire un singolo thread con punti di interruzione condizionali.

Correggere il codice di esempio

  1. Sostituire il GobbleUpBananas metodo con il codice seguente.

     public async Task GobbleUpBananas(int food) // Previously returned void.
     {
         Console.WriteLine("Trying to gobble up food...");
    
         //Task mb = DoSomeMonkeyBusiness();
         //mb.Wait();
         await DoSomeMonkeyBusiness();
     }
    
  2. Nel metodo MorningWalk, chiamare GobbleUpBananas utilizzando await.

    await GobbleUpBananas(myResult);
    
  3. Selezionare il pulsante Riavvia (CTRL+ MAIUSC + F5) e quindi premere F5 più volte finché l'app non sembra bloccarsi.

  4. Premere Interrompi tutto.

    Questa volta, GobbleUpBananas viene eseguito in modo asincrono. Quando si interrompe, viene visualizzato lo stack di chiamate asincrono.

    Screenshot del contesto del debugger dopo la correzione del codice.

    La finestra del call stack è vuota, ad eccezione della voce ExternalCode.

    L'editor di codice non mostra nulla, ma fornisce un messaggio che indica che tutti i thread eseguono codice esterno.

    Tuttavia, la visualizzazione Attività fornisce informazioni utili. DoSomeMonkeyBusiness si trova in cima allo stack di chiamate asincrone, come previsto. Questo indica correttamente dove si trova il metodo a esecuzione prolungata. Ciò è utile per isolare le problematiche async/await quando lo stack delle chiamate fisico nella finestra dello stack delle chiamate non fornisce dettagli sufficienti.

Riassunto

Questa procedura dettagliata ha illustrato la finestra del debugger Parallel Stacks . Usare questa finestra nelle app che usano il modello async/await.