Condividi tramite


Eseguire il debug di un deadlock usando la visualizzazione Thread

Questa esercitazione illustra come usare la visualizzazione Thread delle finestre Stack paralleli per eseguire il debug di un'applicazione multithreading. Questa finestra consente di comprendere e verificare il comportamento di runtime del codice multithreading.

La visualizzazione Thread è supportata per C#, C++e Visual Basic. Il codice di esempio viene fornito per C# e C++, ma alcuni riferimenti al codice e le illustrazioni si applicano solo al codice di esempio C#.

La visualizzazione Thread consente di:

  • Visualizzare le visualizzazioni dello stack di chiamate per più thread, che offre un'immagine più completa dello stato dell'app rispetto alla finestra Stack di chiamate, che mostra solo lo stack di chiamate per il thread corrente.

  • Consente di identificare i problemi, ad esempio thread bloccati o deadlock.

Stack delle chiamate multithread

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

L'animazione concettuale seguente mostra come il raggruppamento viene applicato agli stack di chiamate. Vengono raggruppati solo segmenti identici di uno stack di chiamate. Passare il puntatore del mouse su uno stack di chiamate raggruppate per identificare i thread.

Illustrazione del raggruppamento di stack di chiamate.

Panoramica del codice di esempio (C#, 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 Thread della finestra Stack paralleli per eseguire il debug di un'applicazione multithreading.

L'esempio include un deadlock, che si verifica quando due thread sono in attesa l'uno dell'altro.

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

Per creare il progetto:

  1. Aprire Visual Studio e creare un nuovo progetto.

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

    Nella finestra Start scegliere Nuovo progetto.

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

    Dopo aver applicato i filtri lingua e piattaforma, scegliere l'app console per la lingua scelta e quindi scegliere Avanti.

    Note

    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 un progetto .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 (o .cpp) 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 Multithreaded_Deadlock
     {
         class Jungle
         {
             public static readonly object tree = new object();
             public static readonly object banana_bunch = new object();
             public static Barrier barrier = new Barrier(2);
    
             public static int FindBananas()
             {
                 // Lock tree first, then banana
                 lock (tree)
                 {
                     lock (banana_bunch)
                     {
                         Console.WriteLine("Got bananas.");
                         return 0;
                     }
                 }
             }
    
             static void Gorilla_Start(object lockOrderObj)
             {
                 Debugger.Break();
                 bool lockTreeFirst = (bool)lockOrderObj;
                 Gorilla koko = new Gorilla(lockTreeFirst);
                 int result = 0;
                 var done = new ManualResetEventSlim(false);
    
                 Thread t = new Thread(() =>
                 {
                     result = koko.WakeUp();
                     done.Set();
                 });
                 t.Start();
                 done.Wait();
             }
    
             static void Main(string[] args)
             {
                 List<Thread> threads = new List<Thread>();
                 // Start two threads with opposite lock orders
                 threads.Add(new Thread(Gorilla_Start));
                 threads[0].Start(true);  // First gorilla locks tree then banana
                 threads.Add(new Thread(Gorilla_Start));
                 threads[1].Start(false); // Second gorilla locks banana then tree
    
                 foreach (var t in threads)
                 {
                     t.Join();
                 }
             }
         }
    
         class Gorilla
         {
             private readonly bool lockTreeFirst;
    
             public Gorilla(bool lockTreeFirst)
             {
                 this.lockTreeFirst = lockTreeFirst;
             }
    
             public int WakeUp()
             {
                 int myResult = MorningWalk();
                 return myResult;
             }
    
             public int MorningWalk()
             {
                 Debugger.Break();
                 if (lockTreeFirst)
                 {
                     lock (Jungle.tree)
                     {
                         Jungle.barrier.SignalAndWait(5000); // For thread timing consistency in sample
                         Jungle.FindBananas();
                         GobbleUpBananas();
                     }
                 }
                 else
                 {
                     lock (Jungle.banana_bunch)
                     {
                         Jungle.barrier.SignalAndWait(5000); // For thread timing consistency in sample
                         Jungle.FindBananas();
                         GobbleUpBananas();
                     }
                 }
                 return 0;
             }
    
             public void GobbleUpBananas()
             {
                 Console.WriteLine("Trying to gobble up food...");
                 DoSomeMonkeyBusiness();
             }
    
             public void DoSomeMonkeyBusiness()
             {
                 Thread.Sleep(1000);
                 Console.WriteLine("Monkey business done");
             }
         }
     }
    
  4. Scegliere Salva tutto dal menu File.

  5. Scegliere Compila soluzione dal menu Compila.

Usare la visualizzazione Thread della finestra Stack paralleli

Per avviare il debug:

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

    Note

    In C++, il debugger viene sospeso in __debug_break(). Il resto dei riferimenti al codice e le illustrazioni di questo articolo sono per la versione C#, ma gli stessi principi di debug si applicano a C++.

  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 un secondo thread.

    Tip

    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 gestire questo comportamento a scopo di debug, è possibile aggiungere punti di interruzione aggiuntivi, punti di interruzione condizionali o usare Interrompi tutto. Per altre informazioni sull'uso di punti di interruzione condizionali, vedere Seguire un singolo thread con punti di interruzione condizionali.

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

    Screenshot della visualizzazione Thread nella finestra Stack paralleli.

    In vista Thread, lo stack frame e il percorso di chiamata del thread corrente sono evidenziati in blu. La posizione corrente del thread viene visualizzata dalla freccia gialla.

    Si noti che l'etichetta per lo stack di chiamate per Gorilla_Start è 2 thread. Quando hai premuto F5 per ultima volta, hai avviato un altro thread. Per semplificare le app complesse, gli stack di chiamate identici vengono raggruppati in una singola rappresentazione visiva. Ciò semplifica le informazioni potenzialmente complesse, in particolare negli scenari con molti thread.

    Durante il debug, è possibile attivare o disattivare la visualizzazione di codice esterno. Per attivare o disattivare la funzionalità, 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 F5 di nuovo e il debugger si ferma nella riga Debugger.Break() nel metodo MorningWalk.

    Nella finestra Stack Paralleli viene visualizzata la posizione del thread corrente in esecuzione nel metodo MorningWalk.

    Screenshot della visualizzazione Thread dopo F5.

  5. Passare il puntatore del mouse sul metodo MorningWalk per ottenere informazioni sui due thread rappresentati dallo stack di chiamate raggruppato.

    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. Questo non modifica il thread corrente in esecuzione, ma solo il contesto del debugger.

    In alternativa, è possibile cambiare il contesto del debugger facendo doppio clic su un metodo nella visualizzazione Thread oppure facendo clic con il pulsante destro del mouse su un metodo nella visualizzazione Thread e scegliendo Passa a Frame>[ID thread].

  6. Premere di nuovo F5 e il debugger si sospende nel MorningWalk metodo per il secondo thread.

    Screenshot della visualizzazione Thread dopo il secondo F5.

    A seconda dell'intervallo di esecuzione del thread, a questo punto vengono visualizzati stack di chiamate separati o raggruppati.

    Nella figura precedente gli stack di chiamate per i due thread sono parzialmente raggruppati. I segmenti identici degli stack di chiamate sono raggruppati e le linee di direzione puntano ai segmenti separati , ovvero non identici. Lo stack frame corrente è indicato dall'evidenziazione blu.

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

    Il ritardo è causato da un deadlock. Non viene visualizzato alcun elemento nella visualizzazione Thread perché anche se i thread potrebbero essere bloccati non sono attualmente sospesi nel debugger.

    Note

    In C++, viene visualizzato anche un errore di debug che indica che abort() è stato chiamato.

    Tip

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

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

    Screenshot della visualizzazione Threads dopo aver selezionato Interrompi tutto.

    Nella parte superiore dello stack di chiamate nella vista Thread, si mostra che FindBananas è in deadlock. Il puntatore di esecuzione in FindBananas è una freccia verde a ricciolo, che segnala il contesto del debugger corrente e ci informa anche che i thread non sono attualmente in esecuzione.

    Note

    In C++, non vengono visualizzate le informazioni e le icone utili per il "deadlock rilevato". Tuttavia, si trova ancora la freccia verde curled in Jungle.FindBananas, che indica la posizione del deadlock.

    Nell'editor di codice è presente la freccia verde ricciolita nella lock funzione . I due thread vengono bloccati sulla funzione lock nel metodo FindBananas.

    Screenshot dell'editor di codice dopo aver selezionato Interrompi tutto.

    A seconda dell'ordine di esecuzione del thread, il deadlock viene visualizzato nell'istruzione lock(tree) o lock(banana_bunch) .

    La chiamata a lock blocca i thread nel FindBananas metodo . Un thread è in attesa del rilascio del blocco tree dall'altro thread, ma l'altro thread è in attesa del rilascio del blocco banana_bunch prima che possa rilasciare il blocco su tree. Questo è un esempio di deadlock classico che si verifica quando due thread sono in attesa l'uno sull'altro.

    Se si usa Copilot, è anche possibile ottenere riepiloghi dei thread generati dall'intelligenza artificiale per identificare potenziali deadlock.

    Screenshot delle descrizioni di riepilogo dei thread di Copilot.

Correggere il codice di esempio

Per correggere questo codice, acquisire sempre più lock in un ordine globale coerente tra tutti i thread. In questo modo si evitano attese circolari ed elimina i deadlock.

  1. Per correggere il deadlock, sostituire il codice in MorningWalk con il codice seguente.

    public int MorningWalk()
    {
        Debugger.Break();
        // Always lock tree first, then banana_bunch
        lock (Jungle.tree)
        {
            Jungle.barrier.SignalAndWait(5000); // OK to remove
            lock (Jungle.banana_bunch)
            {
                Jungle.FindBananas();
                GobbleUpBananas();
            }
        }
        return 0;
    }
    
  2. Riavviare l'app.

Summary

Questa procedura dettagliata ha illustrato la finestra del debugger Parallel Stacks . Usare questa finestra su progetti reali che usano codice multithreading. È possibile esaminare il codice parallelo scritto in C++, C# o Visual Basic.