posts - 308, comments - 340, trackbacks - 11

My Links

News

View Pietro Libro's profile on LinkedIn

DomusDotNet
   DomusDotNet

Pietro Libro

Tag Cloud

Article Categories

Archives

Post Categories

Blogs amici

Links

Sincronizzazione dei thread

Quando si lavora con applicazioni multithread (il multithreading è una tecnica che permette di avere più  flussi di esecuzione contemporanei di uno stesso programma) uno degli obiettivi più importanti è  sincronizzare l'accesso alle risorse condivise. Prima di affrontare il problema utilizzando quanto ci viene messo a disposizione dal .Net Framework 2.0 e superiori, torniamo alle origini, partendo  dalle definizioni di programma, processo e thread. Un programma è composto dal codice oggetto, la traduzione del codice sorgente in linguaggio  interpretabile dalla macchina, e può essere costituito da uno o più file. Durante l'esecuzione, il programma rimane immutato. Il processo è l'entità utilizzata dal sistema operativo per rappresentare l'esecuzione  del programma, che a differenza di quest'ultimo, è dinamica. Fra la moltitudine d'informazioni contenute  dall'immagine del processo, vi sono i thread, che definiamo come minima unità in cui può essere diviso un processo. I thread possono essere eseguiti in parallelo, concorrentemente ad altri thread all'interno dello stesso processo; inizialmente, ogni processo è costituito da un singolo thread.

Una delle principali differenze che si hanno tra processi e thread è il modo in cui condividono le risorse:  nel caso dei processi, ognuno di essi ha una propria area di memoria su cui lavorare mentre nel caso dei  thread la memoria di lavoro è la stessa e condividono le stesse informazioni di stato. Quali sono i problemi che si possono verificare nell'utilizzo dei thread nelle nostre applicazioni?  Uno dei principali è sicuramente il "deadlock", ovvero una situazione in cui due o più thread si bloccano a vicenda perchè tentano di accedere ad una risorsa (ad esempio un file) che serve agli altri thread per continuare ad eseguire l'esecuzione del codice. Un piccolo esempio: supponiamo di avere due persone, che per mangiare, hanno bisogno di forchetta e coltello. Ogni persona può prendere un oggetto per volta senza rilasciarlo,  supponendo il caso in cui una persona prende la forchetta e l'altra il coltello, le due persone entrano in deadlock perchè per mangiare hanno bisogno di entrambi gli oggetti, ma nessuno dei due gli possiede.

Vediamo ora cosa può accadere nell'uso pratico dei thread. Supponiamo di avere il seguente codice:

1 private int _numeroLinea = 0;
2 3 private void EseguiOperazione(object writer)
4 {
5 try 6 {
7 System.IO.TextWriter textWriter = writer as System.IO.TextWriter;
8 if (textWriter != null)
9 {
10 _numeroLinea += 1;
11 12 for (int i = _numeroLinea; i < _numeroLinea + 10; i++)
13 {
14 textWriter.WriteLine(String.Format("Linea n° {0}", i));
15 System.Threading.Thread.Sleep(10);
16 }
17 _numeroLinea += 10;
18 }
19 }
20 catch (Exception)
21 {
22 throw new ArgumentException(@"L'oggetto writer non è di tipo textWriter");
23 }
24 }

che richiamiamo in questo modo:

1 using (System.IO.FileStream fileStream = new System.IO.FileStream(@"C:\esempio.txt", System.IO.FileMode.Create))
2 {
3 using (System.IO.StreamWriter textWriter = new System.IO.StreamWriter (fileStream )){
4 EseguiOperazione(textWriter);
5 textWriter.Close();
6 }
7 fileStream.Close();
8 }

Il contenuto del file risulterà come desiderato:

Linea n° 1
Linea n° 2
Linea n° 3
...
Linea n° 10

Supponiamo ora di voler scrivere su file 100 linee e di utilizzare 10 thread, ovvero vogliamo che ogni thread scriva 10 linee del file in modo ordinato. Modifichiamo il codice precedente :

1 using (System.IO.FileStream fileStream = new System.IO.FileStream(@"C:\esempio.txt", System.IO.FileMode.Create))
2 {
3 using (System.IO.StreamWriter textWriter = new System.IO.StreamWriter(fileStream))
4 {
5 6 System.Threading.ParameterizedThreadStart threadStart = new System.Threading.ParameterizedThreadStart(EseguiOperazione);
7 8 //Creo una lista di Thread 9 List < System.Threading.Thread > threadList = new List<System.Threading.Thread>(10);
10 11 //Eseguo i thread 12 for (int i = 0; i < 10; i++)
13 {
14 System.Threading.Thread threadOperazione = new System.Threading.Thread(threadStart);
15 threadList.Add(threadOperazione);
16 threadOperazione.Start(textWriter);
17 }
18 //Attendo che tutti abbiano terminato il loro lavoro 19 for(int i= 0;i<10;i++) threadList[i].Join();
20 21 textWriter.Close();
22 }
23 fileStream.Close();
24 }

Eseguiamo il codice, e sorpresa (per dire), il contenuto del file non è proprio quello che ci aspettavamo , dato che otteniamo qualcosa del tipo:

Linea n° 6
Linea n° 10
Linea n° 7
Linea n° 11
Linea n° 8
Linea n° 12
Linea n° 13
Linea n° 9
Linea n° 14
Linea n° 10
Linea n° 7
Linea n° 11
Linea n° 8

Il problema nasce dal fatto che non abbiamo sincronizzato l'accesso alla funzione EseguiOperazione e quindi più thread hanno avuto accesso parallelamente (in caso di CPU singole il parallelismo è solo simulato)  al codice. Il .Net Framework  mette a disposizione degli sviluppatori una serie di metodi di sincronizzazione che permettono la risoluzione di questi problemi. Il primo è l'utilizzo della keyword lock in C#, SyncLock in VB.NET). Modifichiamo il codice per utilizzare i lock di sincronizzazione:

1 lock (this)
2 {
3 _numeroLinea += 1;
4 5 for (int i = _numeroLinea; i < _numeroLinea + 10; i++)
6 {
7 textWriter.WriteLine(String.Format("Linea n° {0}", i));
8 System.Threading.Thread.Sleep(10);
9 }
10 _numeroLinea += 9;
11 }

Eseguendo il codice, il contenuto del file sarà come atteso. L'uso di semplici keywords ci ha permesso di risolvere il problema in modo molto semplice, ma se avessimo bisogno di più controllo?. Ecco allora la classe Monitor che offre una serie di metodi statici per effettuare la sincronizzazione tra thread.

Monitor

La classe Monitor espone i seguenti metodi statici:

Enter Crea un lock esclusivo su di uno specifico oggetto
Exit Rilascia un lock esclusivo su di uno specifico oggetto
TryEnter Tenta di acquisire per un cerco intervallo di tempo un lock esclusivo su di uno specifico oggetto
Wait Rilascia il blocco su di uno specifico oggetto ed interrompe il thread finchè riacquisice il blocco
Pulse Notifica ad un thread della coda di attesa che lo stato dell'oggetto bloccato  è stato modificato
PulseAll Notifica a tutti i thread della coda di attesa che lo stato dell'oggetto blocato è stato modificato

Possiamo riadattare il codice precedente per l'uso della classe Monitor in questo modo:

 

1 System.Threading.Monitor.Enter(this);
2 3 try 4 {
5 System.IO.TextWriter textWriter = writer as System.IO.TextWriter;
6 if (textWriter != null)
7 {
8 _numeroLinea += 1;
9 10 for (int i = _numeroLinea; i < _numeroLinea + 10; i++)
11 {
12 textWriter.WriteLine(String.Format("Linea n° {0}", i));
13 System.Threading.Thread.Sleep(10);
14 }
15 _numeroLinea += 9;
16 }
17 }
18 catch (Exception ex)
19 {
20 MessageBox.Show("Errore:" + ex.Message);
21 }
22 finally 23 {
24 System.Threading.Monitor.Exit(this);
25 }

 

E' importante richiamare l'Exit del  Monitor nel blocco di codice del Finally perchè in caso di eccezione, siamo sicuri di rilasciare il lock sull'oggetto specifico, concedendo agli altri thread la possibilità di continuare a lavorare (a meno che l'eccezione riguardi tutti i thread e non solo quello corrente)

ReaderWriterLock

Un interessante oggetto per sincronizzare l'accesso a risorse condivise tra più thread è ReaderWriterLock, il quale consente l'accesso simultaneo a più thread per la lettura, mentre consente ad un solo thread per volta l'accesso esclusivo in scrittura. Deduciamo quindi che questo oggetto dovrebbe essere utilizzato in tutte quelle occasioni in cui le operazioni di modifica di una risorsa vengono effettuate raramente. In questo contesto, un thread può avere o un lock in lettura o un lock in scrittura su di una risorsa, ma non entrami contemporaneamente. E' possibile da parte di un thread, acquisire un blocco in scrittura, rilasciando quello in lettura, utilizzando dei metodi appropriati.

La classe ReaderWriterLock, espone i seguenti metodi:

AcquireReaderLock Tenta ti acquisire un lock in lettura, se il lock non può essere ottenuto nel periodo specificato viene sollevata un eccezione di tipo Application.Exception
AcquireWriterLock Tenta di acquisire un lock in scrittura, se il lock non può essere ottenuto nel periodo specificato, viene sollevata un eccezione di tipo Application.Exception
DowngradeFromWriterLock Esegue la conversione da un lock in scrittura in un lock in lettura
UpgradeToWriterLock Tenta l'aggiornamento da un lock in scrittura ad un lock in lettura
ReleaseReaderLock Rilascia un lock in lettura
ReleaseWriterLock Rilascia un lock in scrittura.

I metodi AcquireReaderLock, AcquireWriterLock e UpgradeToWriterLock possono accettare valori di timeout per evitare il verificarsi di situazioni di deadlock. ReaderWriterLock espone due importanti proprietà:

IsReaderLockHeld Ottiene un valore booleano che indica se il thread corrente detiene un lock in lettura
IsWriterLockHeld Ottiene un valore booleano che indica se il thread corrente detiene un lock in scrittura

Supponiamo di voler creare 10 thread che leggono dallo stesso array di stringhe. Ogni thread deve leggere solo una porzione di questo array. Il primo dall'item 0 all'item 9, il secondo dall'item 10 all'item 19 etc... allora potremmo utilizzare la classe ReaderWriterLock in questo modo:

 

1 private void EseguiOperazione2(object arrString)
2 {
3 try 4 {
5 //Tenta di acquisire un lock in lettura 6 _readerWriterLock.AcquireReaderLock(100);
7 8 try 9 {
10 long index = 0;
11 //Tenta di aggiornare il lock in lettura in lock in scrittura 12 System.Threading.LockCookie lockCookie = _readerWriterLock.UpgradeToWriterLock(100);
13 //Aumenta atomicamente il valore di itemIndex 14 System.Threading.Interlocked.Add(ref _itemIndex, 10);
15 //Esegue il Downgrade di lock in lettura 16 _readerWriterLock.DowngradeFromWriterLock(ref lockCookie);
17 //Legge il contenuto dell'array di competenza 18 string[] arrTemp = arrString as string[];
19 for (index = System.Threading.Interlocked.Read(ref _itemIndex); index < _itemIndex + 10; index++)
20 Console.WriteLine(arrTemp[index]);
21 }
22 catch (Exception ex)
23 {
24 //Non può ottenere un lock. Non esegue la lettura 25 MessageBox.Show(ex.Message);
26 }
27 finally 28 {
29 //Rilascia il lock in lettura 30 _readerWriterLock.ReleaseReaderLock();
31 }
32 }
33 catch (ApplicationException ex)
34 {
35 //L'acquisizione del blocco da parte del thread ha raggiunto il timeout 36 MessageBox.Show(ex.Message);
37 }
38 }

 

Dove _readerWriterLock è un istanza di tipo ReaderWriterLock. Nel codice, per eseguire delle operazioni atomiche d'incremento e di lettura di una variabile numerica, utilizziamo la classe Interlocked che espone dei metodi statici per eseguire operazioni d'incremento, decremento e lettura senza interruzioni da parte di altri thread. Il risultato dell'operazione sarà del tipo:

Item#0
Item#1
Item#2
Item#3

...

Item#99

Il .Net Framework offre altri oggetti che ci permettono di sincronizzare l'accesso a risorse condivise tra più thread. Questi sono: Mutex, Event e Semaphore. Tutti e tre sono oggetti del kernel,  introducono un maggiore overhead di utilizzo rispetto alle classi  Monitor e ReaderWriterLock, ma permettono di introdurre dei lock sulle risorse e di notificare un evento a tutti i thread con visibilità nell'ambito dell'intero sistema operativo e quindi superando i limiti dei singoli processi. Le classi corrispondenti agli oggetti su citati sono Mutex, Semaphore, AutoResetEvent, ManualResetEvent. Le prime due classi derivano da WaitHandle, le ultime due da EventWaitHandle la quale deriva da WaitHandle.

Mutex

Se ad esempio provassimo ad eseguire questo codice su più istanze di una stessa applicazione Windows Form composte da un unica Form:

1 private void EseguiOperazione3(object data)
2 {
3 try 4 {
5 //Tenta di ottenere un lock 6 _mutex.WaitOne();
7 8 try 9 {
10 //Recupera l'ultimo valore dell'indice da file 11 int lastLineNumber = int.Parse(System.IO.File.ReadAllText(@"C:\Temp.txt"));
12 //Se il valore di lastLineNumber è maggiore di 100 allora altri thread al di
13 //fuori del processo corrente hanno già terminato di leggere l'array di stringhe 14 if (lastLineNumber < 100)
15 {
16 //Recupera l'oggetto con l'array di stringhe che il thread deve leggere 17 ThreadData threadData = data as ThreadData;
18 19 System.Text.StringBuilder sb = new StringBuilder(100);
20 21 for (int index = lastLineNumber; index < lastLineNumber + 10; index++)
22 sb.AppendFormat("{0} - {1}\r\n", threadData.FormHandle.ToInt64().ToString(), threadData.ArrString[index]);
23 24 //Incrementa il valore dell'indice da cui iniziare a leggere nell'array di stringhe 25 lastLineNumber += 10;
26 //Scrive il valore aggiornato dell'indice su file 27 System.IO.File.WriteAllText(@"C:\Temp.txt", lastLineNumber.ToString());
28 //Esegue l'append nel file di output 29 System.IO.File.AppendAllText(@"C:\output.txt", sb.ToString());
30 //Aggiunge un ritardo di 300 millisecondi 31 System.Threading.Thread.Sleep(300);
32 }
33 }
34 catch (Exception ex)
35 {
36 //Non può ottenere un lock. Non esegue la lettura 37 MessageBox.Show(ex.Message);
38 }
39 finally 40 {
41 //Rilascia il lock in lettura 42 _mutex.ReleaseMutex();
43 }
44 }
45 catch (ApplicationException ex)
46 {
47 //L'acquisizione del blocco da parte del thread ha raggiunto il timeout 48 MessageBox.Show(ex.Message);
49 }
50 }

 

otterremo un file di testo il cui contenuto è del tipo :

1115632 - Item#78
1115632 - Item#79
526404 - Item#80
526404 - Item#81
526404 - Item#82
...

526404 - Item#89
395320 - Item#90

dove ogni riga è composta dall'handle del Form principale dell'applicazione a cui appartiene, e della stringa letta dall'array di stringhe. Dato che vogliamo sincronizzare l'accesso ad una risorsa condivisa tra più applicazioni, è necessario creare un oggetto Mutex denominato, così da ottenerne un istanza quando necessario e crearne una se non esistente:

 

1 try 2 {
3 _mutex = System.Threading.Mutex.OpenExisting("MutexEsempio");
4 }
5 catch (System.Threading.WaitHandleCannotBeOpenedException ex)
6 {
7 //L'oggetto Mutex non esiste... 8 Console.WriteLine("Oggetto MutexEsempio non trovato...");
9 }
10 finally 11 {
12 //Crea l'oggetto Mutex se non esiste 13 if (_mutex == null) _mutex = new System.Threading.Mutex(false, "MutexEsempio");
14 }
15

 

Nel codice, vengono utilizzati due file di appoggio, uno per depositare l'output dell'operazione e l'altro per memorizzare il valore di una variabile condivisa tra più processi. Si potrebbe ad esempio utilizzare una chiave di registro o adottare altre soluzioni.

Semaphore

La classe Semaphore è utilizzata per creare un oggetto del kernel, composto da un contatore che viene decrementato ogni volta che un thread accede al semaforo e viene incrementato ogni volta che un thread rilascia il semaforo. Quando il conteggio è zero, si verifica un blocco delle richieste successive finché il semaforo non viene rilasciato da altri thread. Dopo il rilascio del semaforo da parte di tutti i thread, il conteggio corrisponde al valore massimo specificato al momento della creazione del semaforo. Come per la classe Mutex, un istanza di oggetto semaforo denominata, ha visibilità a livello di sistema operativo, quindi può essere utilizzata per sincronizzare l'accesso tra diversi processi. La creazione di un istanza di oggetto Semaphore è molto semplice ed una volta creato, può essere utilizzata come la classe Mutex:

1 System.Threading.Semaphore semaphore = null;
2 3 try 4 {
5 semaphore = System.Threading.Semaphore.OpenExisting("miosemaforo");
6 }
7 catch (System.Threading.WaitHandleCannotBeOpenedException ex)
8 {
9 //Il semaforo specificato non esiste 10 }
11 finally 12 {
13 if (semaphore == null) semaphore = new System.Threading.Semaphore(0, 10, "semaphore");
14 }

Infine abbiamo le classi AutoResetEvent, ManualResetEvent ed EventWaitHandle che permettono di notificare un evento a più thread. Se si vuole aprire ed utilizzare un evento visibile a livello di sistema operativo è sufficiente creare un istanza della classe EventWaitHandle denominata.

 

Codice sorgente d'esempio

 

Print | posted on domenica 10 febbraio 2008 19.51 |

Feedback

Gravatar

# re: Sincronizzazione dei thread

Bell'articolo, completo e ben documentato. Grazie !
30/05/2008 9.51 | Stefano
Gravatar

# re: Sincronizzazione dei thread

Sono contento che quanto scritto sia stato utile ad altri. Grazie per il feedback.
30/05/2008 9.57 | Pietro Libro
Comments have been closed on this topic.

Powered by: