Condizioni di corsa e deadlock

Visual Basic .NET o Visual Basic offre la possibilità di usare i thread nelle applicazioni Visual Basic per la prima volta. I thread introducono problemi di debug, ad esempio race conditions e deadlock. Questo articolo illustra questi due problemi.

Versione originale del prodotto: Visual Basic, Visual Basic .NET
Numero KB originale: 317723

Quando si verificano condizioni di gara

Una race condition si verifica quando due thread accedono contemporaneamente a una variabile condivisa. Il primo thread legge la variabile e il secondo legge lo stesso valore dalla variabile. Il primo thread e il secondo thread eseguono quindi le operazioni sul valore e corrono per vedere quale thread può scrivere il valore per ultimo nella variabile condivisa. Il valore del thread che scrive l'ultimo valore viene mantenuto, perché il thread sta scrivendo sul valore scritto dal thread precedente.

Dettagli ed esempi per una race condition

A ogni thread viene allocato un periodo di tempo predefinito per l'esecuzione in un processore. Quando il tempo allocato per il thread scade, il contesto del thread viene salvato fino al successivo turno sul processore e il processore inizia l'esecuzione del thread successivo.

Come può un comando di una riga causare una race condition

Esaminare l'esempio seguente per vedere come si verifica una race condition. Sono presenti due thread ed entrambi aggiornano una variabile condivisa denominata total (rappresentata come dword ptr ds:[031B49DCh] nel codice dell'assembly).

  • Thread 1

    Total = Total + val1
    
  • Thread 2

    Total = Total - val2
    

Codice assembly (con numeri di riga) dalla compilazione del codice Visual Basic precedente:

  • Thread 1

    1. mov eax,dword ptr ds:[031B49DCh]
    2. add eax,edi
    3. jno 00000033
    4. xor ecx,ecx
    5. call 7611097F
    6. mov dword ptr ds:[031B49DCh],eax
    
  • Thread 2

    1. mov eax,dword ptr ds:[031B49DCh]
    2. sub eax,edi
    3. jno 00000033
    4. xor ecx,ecx
    5. call 76110BE7
    6. mov dword ptr ds:[031B49DCh],eax
    

Esaminando il codice dell'assembly, è possibile vedere il numero di operazioni eseguite dal processore al livello inferiore per eseguire un semplice calcolo di addizione. Un thread può essere in grado di eseguire tutto o parte del codice dell'assembly durante il tempo sul processore. Esaminare ora come si verifica una race condition da questo codice.

Total è 100, val1 è 50 e val2 è 15. Il thread 1 ha l'opportunità di essere eseguito, ma completa solo i passaggi da 1 a 3. Ciò significa che il thread 1 legge la variabile e completa l'aggiunta. Il thread 1 è ora in attesa di scrivere il nuovo valore di 150. Dopo l'arresto del thread 1 , il thread 2 viene eseguito completamente. Ciò significa che ha scritto il valore calcolato (85) nella variabile Total. Infine, Thread 1 recupera il controllo e termina l'esecuzione. Scrive il valore (150). Pertanto, al termine del thread 1 , il valore di Total è ora 150 anziché 85.

Si può vedere come questo potrebbe essere un problema importante. Se si tratta di un programma bancario, il cliente avrebbe denaro nel proprio conto che non dovrebbe essere presente.

Questo errore è casuale, perché è possibile che il thread 1 completi l'esecuzione prima che scada il tempo sul processore e quindi thread 2 possa iniziare l'esecuzione. Se si verificano questi eventi, il problema non si verifica. L'esecuzione del thread non è deterministica, pertanto non è possibile controllare l'ora o l'ordine di esecuzione. Si noti inoltre che i thread possono essere eseguiti in modo diverso in fase di esecuzione rispetto alla modalità di debug. Inoltre, è possibile notare che se si esegue ogni thread in serie, l'errore non si verifica. Questa casualità rende questi errori molto più difficili da rilevare ed eseguire il debug.

Per evitare che si verifichino condizioni di corsa, è possibile bloccare le variabili condivise, in modo che solo un thread alla volta abbia accesso alla variabile condivisa. Eseguire questa operazione con cautela, perché se una variabile è bloccata nel thread 1 e anche il thread 2 richiede la variabile, l'esecuzione del thread 2 si arresta mentre il thread 2 attende il thread 1 per rilasciare la variabile. Per altre informazioni, vedere SyncLock la sezione Riferimenti di questo articolo.

Sintomi per una race condition

Il sintomo più comune di una race condition è costituito dai valori imprevedibili delle variabili condivise tra più thread. Ciò risulta dall'imprevedibilità dell'ordine in cui vengono eseguiti i thread. A volte vince un thread e a volte vince l'altro thread. In altri casi, l'esecuzione funziona correttamente. Inoltre, se ogni thread viene eseguito separatamente, il valore della variabile si comporta correttamente.

Quando si verificano deadlock

Si verifica un deadlock quando due thread bloccano contemporaneamente una variabile diversa e quindi tentano di bloccare la variabile già bloccata dall'altro thread. Di conseguenza, ogni thread interrompe l'esecuzione e attende che l'altro thread rilasci la variabile. Poiché ogni thread contiene la variabile eseguita dall'altro thread, non si verifica nulla e i thread rimangono deadlock.

Dettagli ed esempi per i deadlock

Il codice seguente include due oggetti LeftVal e RightVal:

  • Thread 1

    SyncLock LeftVal
        SyncLock RightVal
            'Perform operations on LeftVal and RightVal that require read and write.
        End SyncLock
    End SyncLock
    
  • Thread 2

    SyncLock RightVal
        SyncLock LeftVal
            'Perform operations on RightVal and LeftVal that require read and write.
        End SyncLock
    End SyncLock
    

Si verifica un deadlock quando al thread 1 è consentito bloccare LeftVal. Il processore arresta l'esecuzione del thread 1 e inizia l'esecuzione del thread 2. Il thread 2 si blocca RightVal e quindi tenta di bloccare LeftVal. Poiché LeftVal è bloccato, il thread 2 si arresta e attende LeftVal il rilascio. Poiché thread 2 è arrestato, thread 1 è consentito continuare l'esecuzione. Il thread 1 tenta di bloccare RightVal ma non può, perché il thread 2 lo ha bloccato. Di conseguenza, il thread 1 inizia ad attendere fino a quando RightVal non diventa disponibile. Ogni thread attende l'altro thread, perché ogni thread ha bloccato la variabile in attesa dell'altro thread e nessuno dei due thread sblocca la variabile che contiene.

Un deadlock non si verifica sempre. Se thread 1 esegue entrambi i blocchi prima che il processore lo arresti, Thread 1 può eseguire le operazioni e quindi sbloccare la variabile condivisa. Dopo che thread 1 sblocca la variabile, Thread 2 può procedere con la relativa esecuzione, come previsto.

Questo errore sembra ovvio quando questi frammenti di codice vengono affiancati, ma in pratica il codice può essere visualizzato in moduli o aree separate del codice. Si tratta di un errore difficile da rilevare perché, da questo stesso codice, possono verificarsi sia l'esecuzione corretta che l'esecuzione errata.

Sintomi per i deadlock

Un sintomo comune del deadlock è che il programma o il gruppo di thread smette di rispondere. Questo è noto anche come blocco. Almeno due thread sono in attesa di una variabile bloccata dall'altro thread. I thread non procedono perché nessuno dei due thread rilascerà la variabile finché non ottiene l'altra variabile. L'intero programma può bloccarsi se il programma è in attesa di uno o entrambi i thread per completare l'esecuzione.

Che cos'è un thread

I processi vengono usati per separare le diverse applicazioni in esecuzione in un determinato momento in un singolo computer. Il sistema operativo non esegue processi, ma lo fanno i thread. Un thread è un'unità di esecuzione. Il sistema operativo alloca il tempo del processore a un thread per l'esecuzione delle attività del thread. Un singolo processo può contenere più thread di esecuzione. Ogni thread mantiene i propri gestori eccezioni, le priorità di pianificazione e un set di strutture usate dal sistema operativo per salvare il contesto del thread se il thread non riesce a completarne l'esecuzione durante il tempo assegnato al processore. Il contesto viene mantenuto fino alla successiva ricezione del tempo del processore da parte del thread. Il contesto include tutte le informazioni richieste dal thread per continuare senza problemi l'esecuzione. Queste informazioni includono il set di registri del processore del thread e lo stack di chiamate all'interno dello spazio indirizzi del processo host.

Riferimenti

Per altre informazioni, cercare le parole chiave seguenti nella Guida di Visual Studio:

  • SyncLock. Consente di bloccare un oggetto. Se un altro thread tenta di bloccare lo stesso oggetto, viene bloccato fino al rilascio del primo thread. Usare SyncLock con attenzione, perché i problemi possono derivare dall'uso improprio di SyncLock. Ad esempio, questo comando può impedire le race condition, ma causare deadlock.

  • InterLocked. Consente di selezionare un set di operazioni thread-safe sulle variabili numeriche di base.

Per altre informazioni, vedere Thread e Threading.