- Multi-threading e data race:
monitor e SyncLock per l'accesso a dati condivisi -
|
|||
COSA SERVE PER QUESTO TUTORIAL | |||
Download | Chiedi sul FORUM | Glossario | un server che supporti ASP .Net | ||
Problematiche del multi-threading in .Net | |||
IL PROBLEMA Accesso a dati condivisi da più thread paralleli. Come detto nel precedente articolo i problemi con i thread sorgono quando si ha la necessità di accedere alla stessa risorsa (tipicamente una variabile) da due thread differenti in esecuzione parallela. Quando si scrive codice che deve leggere o scrivere su dati a cui potrebbe voler accedere anche un altro thread bisogna sempre tener presente che la possibilità che tra un'istruzione e l'altra il thread si interrompa e passi il controllo ad un altro. Questo secondo thread potrebbe alterare i dati su cui il primo stava lavorando creando comportamenti inattesi (tipicamente si verifica una data race, una corsa ai dati). Prendiamo un esempio classico: Imports System.Threading Module Main Const intSomma As Integer = 100 Private intTotale As Integer = 0 Sub Main() Console.Write("Numero di thread da creare: ") Dim intThreadTotal As Integer = CInt(Console.ReadLine()) Dim thrSommatori(intThreadTotal) As Thread 'Creiamo il numero di thread specificato, tutti sulla funzione Somma For C1 As Integer = 0 To intThreadTotal - 1 thrSommatori(C1) = New Thread(AddressOf Somma) Next C1 'Avviamo tutti i thread For C1 As Integer = 0 To intThreadTotal - 1 thrSommatori(C1).Start() Next C1 'Attendiamo la terminazione di tutti i thread For C1 As Integer = 0 To intThreadTotal - 1 thrSommatori(C1).Join() Next C1 'Stampiamo il risultato ottenuto e quello previsto Console.WriteLine("Risultato: {0} (atteso: {1})", intTotale, intSomma * intThreadTotal) Console.ReadLine() End Sub 'Funzione che effettua somme Public Sub Somma() For CSum As Integer = 1 To intSomma intTotale += 1 Next CSum End Sub End Module Questa semplice applicazione console non fa altro che creare un numero di thread specificato dall'utente, avviarli e attendere la loro terminazione. I thread incrementano di 1 100 volte una variabile globale. Se si prova ad eseguire il programma specificando un numero di thread sufficientemente elevato (provare con 10 e poi a salire di ordini di grandezza) si vedrà che il risultato finale non sarà quello atteso: ad esempio sulla macchina su cui è stato testato, con 1000 thread, il risultato finale era 99'800 invece che 100'000. Perché questo strano comportamento? Analizziamo la riga che crea il problema: intTotale +=1 Niente di più semplice apparentemente, ma per capire bene dove nasce il problema dobbiamo entrare in profondità in questa istruzione: per prima cosa legge la variabile intTotale, effettua la somma e infine scrive il risultato in intTotale. È quindi equivalente al seguente codice: Dim intTemp As Integer = intTotale intTemp = intTemp + 1 intTotale = intTemp
L'errore si verifica per via del fatto che nel tempo che trascorre tra
la lettura di intTotale e la sua riscrittura in memoria, un altro thread ha
incrementato quella stessa variabile, ma di questo fatto il thread che era in
esecuzione precedentemente non può essersi accorto e quindi l'incremento
del secondo thread viene perso con la scrittura del nuovo valore. Come forzare la scrittura e lettura di una variabile dalla memoria principale e assicurarsi di avervi accesso esclusivo.
Per ovviare a questo problema ci si può servire dei monitor. In .Net ad
ogni oggetto è associato (in teoria) un monitor, che può essere
acquisito,
così impedendo l'accesso a quell'oggetto da parte, e rilasciato, rendendo
di nuovo disponibile l'oggetto ad altri thread. Acquisire il monitor
(altresì detto lock) su una variabile significa quindi
assicurarsi che fino a quando non lo si rilascerà, i dati della
variabile non saranno
utilizzati da nessun altro thread (che verrà quindi messo in stato di
attesa fino al rilascio). Per acquisire un lock ci si server di Monitor.Enter
e per rilasciarlo di Monitor.Exit. Si noti inoltre che una
chiamata a Monitor.Enter sortisce anche l'effetto di forzare una
lettura dalla memoria principale (una lettura che si definisce
volatile) e viceversa Monitor.Exit forza una scrittura sulla
memoria principale (scrittura che si definisce volatile), in modo
da non avere versioni diverse della stessa variabile nei vari livelli di
memoria. Private objMonitor As New Object() Modifichiamo quindi la routine Somma: Public Sub Somma() For CSum As Integer = 1 To intSomma Monitor.Enter(objMonitor) intTotale += 1 Monitor.Exit(objMonitor) Next CSum End Sub Se ora riproviamo ad eseguire il codice, otterremo il risultato sperato, ovvero nessun incremento è stato perso. CONSIDERAZIONI FINALIMonitor, eccezioni e la parola chiave SyncLock Per imporre un lock su una variabile, Visual Basic, mette a disposizione anche un metodo più pulito, sicuro ed elegante rispetto a Monitor.Enter e Monitor.Exit, ovvero la parola chiave SyncLock. SyncLock definisce un blocco (simile a Using) che fa le veci delle chiamate a Monitor.Enter e Monito.Exit, e inoltre concepisce anche il caso in cui si generi un'eccezione. Infatti, se per qualche ragione l'incremento dovesse generare un'eccezione (ad esempio di overflow) il lock non verrebbe mai rilasciato nella versione del codice precedente, il che comporterebbe che tutti i thread in attesa per la disponibilità di objMonitor rimarrebbero inesorabilmente bloccati per sempre. Dovremmo dunque scrivere: Public Sub Somma() For CSum As Integer = 1 To intSomma Monitor.Enter(objMonitor) Try intTotale += 1 Finally Monitor.Exit(objMonitor) End Try Next CSum End Sub Che è del tutto equivalente alla seguente sintassi: Public Sub Somma() For CSum As Integer = 1 To intSomma SyncLock objMonitor intTotale += 1 End SyncLock Next CSum End Sub
|
|||
<< INDIETRO | by VeNoM00 |