Questo sito utilizza cookies solo per scopi di autenticazione sul sito e nient'altro. Nessuna informazione personale viene tracciata. Leggi l'informativa sui cookies.
Username: Password: oppure
Guida al Visual Basic .NET - Multithreading  Parte II

Guida al Visual Basic .NET

Capitolo 88° - Multithreading Parte II

<< Precedente Prossimo >>

Avendo a che fare con i thread, diventa difficoltoso sincronizzare l'accesso alle risorse. Mi spiego meglio. Si ponga di avere questo codice:

If Str = "Ciao" Then
  I += 1
  Str = Nothing
End If 

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 è:

SyncLock [Oggetto di lock]
  'Istruzioni sincronizzate
End SyncLock 

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:

'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 LockObject As New Object()

'...

'Questo blocco è ora correttamente sincronizzato
SyncLock LockObject
    If Str = "Ciao" Then
        I += 1
    End If
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).

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 AnObject
    Inherits 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 LockObject As New Object()
'...
Try
    'Entra nel codice sincronizzato
    Monitor.Enter(LockObject)
    '...
Catch Ex As 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

È possibile richiamare WaitOne più volte, a patto che si richiamai lo stesso numero di volte ReleaseMutex.

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:

'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 Dir As String, _
    ByVal Pattern As String) As List(Of String) 

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:

Module Module1
    Public Delegate Function GetFileRecursive(ByVal Dir As String, _
        ByVal Pattern As String) As List(Of String)

    Public Function FindFiles(ByVal Dir As String, _
        ByVal Pattern As String) As List(Of String)
        Dim Result As 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 SubDir As String In IO.Directory.GetDirectories(Dir)
            Result.AddRange(FindFiles(SubDir, Pattern))
        Next

        Return Result
    End Function

    Sub Main()
        'Nuovo oggetto di tipo delegate GetFileRecursive
        Dim Find As 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 AsyncRes As IAsyncResult = _ 
            Find.BeginInvoke("C:WINDOWSsystem32", "*.dll", _ 
            Nothing, Nothing)
        'Risultato della ricerca
        Dim Files As 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 

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.

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:

Module Module1
    Public Delegate Function GetFileRecursive(ByVal Dir As String, _ 
        ByVal Pattern As String) As List(Of String)

    Public Function FindFiles(ByVal Dir As String, _
        ByVal Pattern As String) As List(Of String)
        Dim Result As New List(Of String)

        Result.AddRange(IO.Directory.GetFiles(Dir, Pattern))
        For Each SubDir As String In IO.Directory.GetDirectories(Dir)
            Result.AddRange(FindFiles(SubDir, Pattern))
        Next

        Return Result
    End Function

    Public Sub DisplayFiles(ByVal AsyncRes As IAsyncResult)
        Dim Files As 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 Find As New GetFileRecursive(AddressOf FindFiles)

    Sub Main()
        'Il terzo argomento è l'indirizzo del metodo callback
        Dim AsyncRes As IAsyncResult = _ 
            Find.BeginInvoke("C:WINDOWSsystem32", _
            "*.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 

L'ultimo parametro specifica solamente delle informazioni aggiuntive richiamabili con IAsyncResult.AsyncState.

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.

<< Precedente Prossimo >>
A proposito dell'autore

C#, TypeScript, java, php, EcmaScript (JavaScript), Spring, Hibernate, React, SASS/LESS, jade, python, scikit, node.js, redux, postgres, keras, kubernetes, docker, hexo, etc...