Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
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.
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:
- Crea un oggetto che rappresenta un gorilla.
- Gorilla si sveglia.
- Gorilla va in una passeggiata di mattina.
- Gorilla trova banane nella giungla.
- Gorilla mangia.
- Gorilla si impegna in attività da furfante.
Creare il progetto di esempio
Per creare il progetto:
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.
Aprire il file di codice .cs (o .cpp) nel progetto. Eliminare il contenuto per creare un file di codice vuoto.
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"); } } }Scegliere Salva tutto dal menu File.
Scegliere Compila soluzione dal menu Compila.
Usare la visualizzazione Thread della finestra Stack paralleli
Per avviare il debug:
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++.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.
Selezionare Debug > stack paralleli di Windows > per aprire la finestra Stack paralleli e quindi selezionare Thread dall'elenco a discesa Visualizza nella finestra.
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.
Premere F5 di nuovo e il debugger si ferma nella riga
Debugger.Break()nel metodoMorningWalk.Nella finestra Stack Paralleli viene visualizzata la posizione del thread corrente in esecuzione nel metodo
MorningWalk.
Passare il puntatore del mouse sul metodo
MorningWalkper ottenere informazioni sui due thread rappresentati dallo stack di chiamate raggruppato.Il thread corrente viene visualizzato anche nell'elenco Thread sulla barra degli strumenti 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].
Premere di nuovo F5 e il debugger si sospende nel
MorningWalkmetodo per il secondo thread.
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.
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.
Nella parte superiore dell'IDE sulla barra degli strumenti Debug selezionare il pulsante Interrompi tutto (icona pausa) o premere CTRL+ALT+INTERR.
Nella parte superiore dello stack di chiamate nella vista Thread, si mostra che
FindBananasè in deadlock. Il puntatore di esecuzione inFindBananasè 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
lockfunzione . I due thread vengono bloccati sulla funzionelocknel metodoFindBananas.A seconda dell'ordine di esecuzione del thread, il deadlock viene visualizzato nell'istruzione
lock(tree)olock(banana_bunch).La chiamata a
lockblocca i thread nelFindBananasmetodo . Un thread è in attesa del rilascio del bloccotreedall'altro thread, ma l'altro thread è in attesa del rilascio del bloccobanana_bunchprima che possa rilasciare il blocco sutree. 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.
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.
Per correggere il deadlock, sostituire il codice in
MorningWalkcon 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; }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.