C9. Multithreading - Parte II
Avendo a che fare con i thread, diventa difficoltoso sincronizzare l'accesso alle risorse. Mi spiego meglio. Si ponga di avere questo
codice:
Come alternativa a SyncLock, esiste l'oggetto Monitor, che espone metodi statici per la sincronizzazione. Enter accetta un argomento, che costituisce l'oggetto di lock, e incrementa il contatore di lock di 1, cosicchè gli altri thread che tentino di accedere al codice successivo a Enter debbano attendere (esattamente come accade con SyncLock). Exit esce dal blocco sincronizzato, mentre TryEnter cerca di entrare e restituisce False se non è possibile accedere al blocco monitorato entro un timeout specificato come primo argomento. Dato che è essenziale rilasciare sempre il lock, se il sorgente ha la possibilità di lanciare un'eccezione, bisogna necessariamente usare un costrutto Try nella cui clausola Finally si richiama Exit. Ad esempio:
Il tipo Mutex, invece, è più versatile: intanto può essere istanziato, e inoltre espone metodi d'istanza in grado di gestire più lock contemporaneamente. Eccone un elenco:
Il tipo Semaphore, invece, controlla che un determinato numero di thread possa eseguire un dato blocco di codice sincronizzato. Il suo costruttore accetta come primo parametro un intero che indica il valore di default dei thread che lo stanno eseguendo e come secondo parametro il conteggio massimo. Al suo interno, ogni volta che un thread ottiene il lock della sezione controllata, il contatore viene decrementato di 1, fino al raggiungimento del valore di default; ogni volta che si rilascia il lock, esso viene incrementato di 1, fino al raggiungimaneto del valore massimo. WaitOne() serve per acquisire il lock e Release per rilasciarlo.
N.B.: Tutti i tipi fin'ora esposti (Monitor, Mutex e Semaphore) devono sempre essere inclusi in un blocco Try, per assicurarsi che anche se si verificassero delle eccezioni, il lock venga comunque rilasciato.
Analizziamo ora il penultimo parametro di BeginInvoke. È un delegate di tipo System.AsyncCallback e costituisce il metodo di callback. Questi tipi di metodi vengono automaticamente richiamati dal programma alla fine delle operazioni nel thread separato: così facendo non si deve continuamente controllarne il completamento con IAsyncResult.IsCompleted. La sua signature deve rispecchiare quella di AsyncCallback, ossia deve accettare un unico parametro di tipo IAsyncResult. Ecco lo stesso esempio di prima riscritto usando questa tecnica:
In generale, tutti i metodi che vengono resi asincroni, dispongono di due versioni, una che inizia per "Begin", l'altra che inizia per "End", con le stesse caratteristiche sopra esposte. Anche i metodi BeginWrite e EndWrite di IO.FileStream sono ottimi esempi di metodi asincroni.
Ora, per ipotesi Str è una variabile condivisa fra thread, così come anche I; sempre per ipotesi, Str ha assunto il valore "Ciao" prima di entrare nel blocco If. Il thread A controlla la variabile e trova, giustamente, che Str è uguale a "Ciao": nessun problema, prosegue all'interno della struttura e incrementa I di uno. Proprio dopo il termine di quest'ultima operazione, scade il suo timeslice, e il gestore dei thread concede al thread B la sua parte di tempo macchina. Quest'ultimo thread vede che Str è ancora uguale alla costante stringa specificata dal programmatore, in quanto A si era interrotto subito prima di passare all'istruzione successiva, ossia Str = Nothing: per logica, a sua volta incrementa I di un'altra unità e poi prosegue normalmente annullando Str. Al termine del blocco si ha che I è stato incrementato di due anzichè di uno. Problemi del genere sono in genere rari, ma si possono comunque verificare e la probabilità di incontrarli aumenta parallelamente all'impiego del meccanismo di threading. Per risolvere errori come questi si deve sincronizzare l'accesso alle risorse e si fa uso dello statement SyncLock. Esso ha il compito di racchiudere un'area di codice in un blocco unico, in modo che il thread che lo sta eseguendo finisca tutte le operazioni ivi contenute senza essere disturbato da altri thread, i quali a loro volta attenderanno di potervi accedere. La sintassi usata per dichiarare SyncLock è:If Str = "Ciao"Then I += 1 Str = NothingEnd If
L'oggetto di lock può essere un qualsiasi oggetto reference non nullo condiviso tra i thread (ad esempio una variabile di modulo o di classe o una variabile statica a cui non sia stato applicato l'attributo ThreadStatic): una volta entrati nel blocco SyncLock, l'oggetto viene, per così dire, "segnato", in modo che qualsiasi altro thread che cerchi di accedervi saprà che è attualmente in uso e attenderà il proprio turno. Non è importante quale sia l'oggetto di lock, nè lo è il suo tipo: basta che soddisfi i requisiti sopra esposti. In una classe si può benissimo usare Me al suo posto. Ad esempio:SyncLock [Oggetto di lock] 'Istruzioni sincronizzate End SyncLock
'Tuttavia, la sincronizzazione mediata dal costrutto SyncLock è da utilizzarsi solo se veramente indispensabile, poichè racchiudere tutti i campi o tutti i metodi in un blocco del genere rischia di rendere il codice sia illeggibile sia più lento e meno economico. Un'altra particolarità di SyncLock è che, dietro le quinte, il compilatore lo implementa inserendovi all'interno uno statement Try, per evitare di non poter rilasciare il lock qualora si verifichino eccezioni, ragion per cui non si può saltarvi all'interno con l'utilizzo di GoTo (vedi capitolo relativo).Volendo riprendere l'esempio di prima: 'LockObject è condivisa (campo di classe, in più shared), non nulla '(viene usato il costruttore New) e reference (ovviamente, 'Object è reference) Private Shared LockObjectAs New Object() '... 'Questo blocco è ora correttamente sincronizzato SyncLock LockObjectIf Str = "Ciao"Then I += 1End If End SyncLock
Altri metodi di sincronizzazione
Se un intero oggetto viene esposto alla possibilità di poter venire manipolato da più thread contemporaneamente, sarebbe utile applicarvi
un attributo speciale che sincronizza automaticamente l'accesso a tutti i membri d'istanza: tale attributo si chiama Synchronization, non
espone alcun costruttore usato frequentemente e appartiene al namespace System.Runtime.Remoting.Contexts:
<System.Runtime.Remoting.Contexts.Synchronization()> _Public Class AnObjectInherits ContextBoundObject 'Altro dettaglio: l'oggetto deve ereditare da ContextBoundObject '... End Class
Come alternativa a SyncLock, esiste l'oggetto Monitor, che espone metodi statici per la sincronizzazione. Enter accetta un argomento, che costituisce l'oggetto di lock, e incrementa il contatore di lock di 1, cosicchè gli altri thread che tentino di accedere al codice successivo a Enter debbano attendere (esattamente come accade con SyncLock). Exit esce dal blocco sincronizzato, mentre TryEnter cerca di entrare e restituisce False se non è possibile accedere al blocco monitorato entro un timeout specificato come primo argomento. Dato che è essenziale rilasciare sempre il lock, se il sorgente ha la possibilità di lanciare un'eccezione, bisogna necessariamente usare un costrutto Try nella cui clausola Finally si richiama Exit. Ad esempio:
Private Shared LockObjectAs New Object() '... Try 'Entra nel codice sincronizzato Monitor.Enter(LockObject) '... Catch ExAs Exception 'Cattura eccezioni se ce ne sono Finally 'Ma rilascia sempre il lock Monitor.Exit()End Try
Il tipo Mutex, invece, è più versatile: intanto può essere istanziato, e inoltre espone metodi d'istanza in grado di gestire più lock contemporaneamente. Eccone un elenco:
- WaitOne : attende di poter entrare nella sezione sincronizzata e, una volta entrato, ottiene il lock per il thread corrente
- WaitAny(M()) : accetta un array di Mutex M() e attende di poter acquisire il lock di almeno uno di essi. Questo metodo può essere usato ad esempio quando si dispone di un numero limitato di risorse (file, connessioni, database...) , ognuna manipolata da un thread differente
- WaitAll(M()) : accetta un array di Mutex M() e attende di poter acquisire il lock di tutti. Questo metodo può essere usato ad esempio quando si devono compiere più operazioni contemporaneamente e aspettare che tutte siano state portate a termine
- ReleaseMutex : rilascia il lock
- SignalAndWait(M1, M2) : tenta di acquisire il lock di M1 e, una volta acquisito, aspetta che anche M2 venga lockato
Il tipo Semaphore, invece, controlla che un determinato numero di thread possa eseguire un dato blocco di codice sincronizzato. Il suo costruttore accetta come primo parametro un intero che indica il valore di default dei thread che lo stanno eseguendo e come secondo parametro il conteggio massimo. Al suo interno, ogni volta che un thread ottiene il lock della sezione controllata, il contatore viene decrementato di 1, fino al raggiungimento del valore di default; ogni volta che si rilascia il lock, esso viene incrementato di 1, fino al raggiungimaneto del valore massimo. WaitOne() serve per acquisire il lock e Release per rilasciarlo.
N.B.: Tutti i tipi fin'ora esposti (Monitor, Mutex e Semaphore) devono sempre essere inclusi in un blocco Try, per assicurarsi che anche se si verificassero delle eccezioni, il lock venga comunque rilasciato.
Delegate asincroni
Altra caratteristica che rende ancor più versatili i delegate è costituita dalla possibilità di invocare metodi asincorni. In questi casi,
il metodo puntato dal delegate viene eseguito in un thread differente, senza quindi bloccare il normale corso di istruzioni del programma, come
d'altronde, sono solite fare tutte le direttive asincrone. Il primo passo da effettuare per creare una procedura del genere è, ovviamente,
dichiarare il delegate corrispondente, ad esempio:
'Il compilatore crea automaticamente due metodi speciali per ogni nuovo delegate dichiarato dal programmatore: essi sono BeginInvoke ed EndInvoke. Il primo accetta come argomenti gli stessi definiti nella signature del delegate (in questo caso Dir As String e Pattern As String); inoltre, la lista dei parametri prosegue con altri due slot che spiegherò in seguito e che per ora imposterò semplicemente a Nothing. Bisogna poi specificare che è una funzione, quindi restituisce un valore: tale valore non è il risultato dell'operazione, ma un oggetto di tipo IAsyncResult (ossia che implementa l'interfaccia IAsyncResult) che serve a fornire informazioni sul progresso del metodo. Tra le sue quattro proprietà, una in particolare, IsCompleted, determina quando il thread che esegue l'operazione ha portato a termine il suo compito. Il secondo accetta semplicemente lo stesso oggetto IAsyncResult ottenuto in precedenza e, una volta sicuri di aver terminato il tutto, restituisce il vero risultato della funzione (se c'e'). Ecco un esempio:Questo delegate accetta i parametri adatti a svolgere una ricerca 'di files in più sottodirectory, esempio già citato in molte 'lezioni precedenti Public Delegate Function GetFileRecursive(ByVal DirAs String , _ByVal PatternAs String )As List(Of String )
All'intero di IAsyncResult è definita anche un'altra proprietà, AsyncWaitHandle, che restituisce un oggetto WaitHandle: dato che da questo deriva Mutex, lo si può trattare come un comunissimo Mutex, appunto, usando i metodi WaitOne, WaitAny o WaitAll sopra esposti.Module Module1Public Delegate Function GetFileRecursive(ByVal DirAs String , _ByVal PatternAs String )As List(Of String )Public Function FindFiles(ByVal DirAs String , _ByVal PatternAs String )As List(Of String )Dim ResultAs New List(Of String ) 'Aggiunge in un solo colpo tutti i files trovati con GetFiles Result.AddRange(IO.Directory.GetFiles(Dir, Pattern)) 'Analizza le altre directory For Each SubDirAs String In IO.Directory.GetDirectories(Dir) Result.AddRange(FindFiles(SubDir, Pattern))Next Return ResultEnd Function Sub Main() 'Nuovo oggetto di tipo delegate GetFileRecursive Dim FindAs New GetFileRecursive(AddressOf FindFiles) 'Con questo oggetto, monitoreremo lo stato del metodo, per sapere 'se è stata completato o se è ancora in esecuzione 'Si cercano tutti i files *.dll in una cartella di sistema Dim AsyncResAs IAsyncResult = _ Find.BeginInvoke("C:\WINDOWS\system32", "*.dll", _Nothing ,Nothing ) 'Risultato della ricerca Dim FilesAs List(Of String ) Console.WriteLine("Ricerca di tutti i files *.dll in System32") 'Finchè non si è completato, scrive a schermo "Ricerca in corso..." Do Until AsyncRes.IsCompleted Console.WriteLine("Ricerca in corso...") Thread.Sleep(2000)Loop 'Ottiene il risultato Files = Find.EndInvoke(AsyncRes) 'Usa il metodo ForEach di Array per eseguire una stessa operazione 'per ogni elemento di un array. Dato che Files è una lista 'tipizzata, la converte in array di stringhe, quindi richiama su 'ogni elemento il metodo Console.WriteLine per scriverlo a schermo Array.ForEach(Files.ToArray,AddressOf Console.WriteLine) Console.ReadKey()End Sub End Module
Analizziamo ora il penultimo parametro di BeginInvoke. È un delegate di tipo System.AsyncCallback e costituisce il metodo di callback. Questi tipi di metodi vengono automaticamente richiamati dal programma alla fine delle operazioni nel thread separato: così facendo non si deve continuamente controllarne il completamento con IAsyncResult.IsCompleted. La sua signature deve rispecchiare quella di AsyncCallback, ossia deve accettare un unico parametro di tipo IAsyncResult. Ecco lo stesso esempio di prima riscritto usando questa tecnica:
L'ultimo parametro specifica solamente delle informazioni aggiuntive richiamabili con IAsyncResult.AsyncState.Module Module1Public Delegate Function GetFileRecursive(ByVal DirAs String , _ByVal PatternAs String )As List(Of String )Public Function FindFiles(ByVal DirAs String , _ByVal PatternAs String )As List(Of String )Dim ResultAs New List(Of String ) Result.AddRange(IO.Directory.GetFiles(Dir, Pattern))For Each SubDirAs String In IO.Directory.GetDirectories(Dir) Result.AddRange(FindFiles(SubDir, Pattern))Next Return ResultEnd Function Public Sub DisplayFiles(ByVal AsyncResAs IAsyncResult)Dim FilesAs List(Of String ) = Find.EndInvoke(AsyncRes) Array.ForEach(Files.ToArray,AddressOf Console.WriteLine)End Sub 'Nuovo oggetto di tipo delegate GetFileRecursive 'Notare che è dichiarato come variabile globale di modulo per essere 'accessibile anche alla procedura callback Dim FindAs New GetFileRecursive(AddressOf FindFiles)Sub Main() 'Il terzo argomento è l'indirizzo del metodo callback Dim AsyncResAs IAsyncResult = _ Find.BeginInvoke("C:\WINDOWS\system32", _ "*.dll",AddressOf DisplayFiles,Nothing ) Console.WriteLine("Ricerca di tutti i files *.dll in System32")Do Until AsyncRes.IsCompleted Console.WriteLine("Ricerca in corso...") Thread.Sleep(2000)Loop Console.ReadKey()End Sub End Module
In generale, tutti i metodi che vengono resi asincroni, dispongono di due versioni, una che inizia per "Begin", l'altra che inizia per "End", con le stesse caratteristiche sopra esposte. Anche i metodi BeginWrite e EndWrite di IO.FileStream sono ottimi esempi di metodi asincroni.
The Totem's Lair - Copyright (C) 2009
È vietata la riproduzione sia totale che parziale del sito.



