Date post: | 14-Apr-2017 |
Category: |
Documents |
Upload: | massimiliano-brolli |
View: | 105 times |
Download: | 1 times |
Scrivere Applicazioni Multi Threading in .NET Di Massimiliano Brolli
Premessa ................................................................................................... 3
Prerequisiti ................................................................................................ 3
Il Multi Threading ....................................................................................... 4
Le basi ...................................................................................................... 5
Il Namespace .......................................................................................... 5
Le Dichiarazioni ....................................................................................... 5
Lanciare un thread ................................................................................... 5
Meccanismi di Sincronizzazione .................................................................... 6
Sleep ...................................................................................................... 6
Start ...................................................................................................... 7
ResetAbort .............................................................................................. 7
Suspend ................................................................................................. 7
Resume .................................................................................................. 7
Join ........................................................................................................ 8
Interrupt ................................................................................................. 8
Abort ...................................................................................................... 9
SpinWait ............................................................................................... 10
Stato dei thread ....................................................................................... 11
La priorità dei thread .............................................................................. 13
Scopo delle variabili ............................................................................... 15
Pool di Thread .......................................................................................... 18
Gli eventi ................................................................................................. 22
I Timer .................................................................................................... 24
Apartment Thread e interoperabilità COM .................................................... 25
Conclusioni .............................................................................................. 27
Bibliografia .............................................................................................. 28
Premessa Molto spesso ci siamo imbattuti nel termine AJAX, acronimo di Asynchronous JavaScript and XML ovvero un sistema che permette alle pagine web di agire in maniera asincrona. Lo scopo di questo documento e far capire tramite semplici esempi le componenti/funzionalità offerte da questo straordinario Framework messo a disposizione anche in ambiente .NET.
Prerequisiti Per procedere alla lettura di questo documento occorre aver letto le seguenti dispense :
• Introduzione agli algoritmi. • La scelta tra Microsoft.NET & Java • Predisposizione Virtual Machine di base • Predisposizione Virtual Machine per l'ambiente di sviluppo Microsoft .NET • Introduzione alla scrittura del codice .NET • Scrivere applicazioni .NET con Visual Studio • Panoramica sulla programmazione ad oggetti • La Storia • Introduzione al Framework .NET • Gli oggetti ADO.NET • Predisposizione Ambiente SQL Server • Applicazioni Windows Forms • ADO.NET Entity Framework • Scrivere ASP.NET Web Application, premessa e predisposizione • Scrivere ASP.NET Web Application, La prima applicazione Web Forms • Scrivere ASP.NET Web Application, I Web Services • Scrivere ASP.NET Web Application, Applicazioni MVC • Scrivere ASP.NET Web Application, AJAX
Il Multi Threading
Il MultiThreading comparso con Windows NT 3.1 e successivamente con Windows 95 rappresenta una rivoluzione nel campo informatico di largo consumo. Solamente i linguaggi di basso livello prima dell’evento del Framework .NET potevano realizzare applicazioni MultiThreading come ad esempio MSVC++ con l’ausilio di particolari librerie. In questo documento andremo ad analizzare le istruzioni che occorre imparare per lavorare con i Thread utilizzando Microsoft.NET Framework 2.0 ed il linguaggio VB.NET. Un Thread o filo in inglese, rappresenta un processo che può lavorare parallelamente al padre chiamante. Nel framework.NET si può far lavorare in un Thread separato porzioni di codice quali Sub Routine o intere classi per realizzare applicazioni performanti che hanno come limite solamente la fantasia. Addentrandoci nel documento cominceremo a familiarizzare con i meccanismi di programmazione Asincrona che introdurranno problematiche nuove quali la concorrenza applicativa e i meccanismi di sincronizzazione.
Le basi
Il Namespace
Per realizzare applicazioni MultiThreading occorre utilizzare il nameSpace System.Threading come viene mostrato nell’esempio successivo. Imports System.Threading
Le Dichiarazioni
Per lavorare con i Thread occorre dichiarare un oggetto di tipo ThreadStart che permette di eseguire lo Start di un Thread e un oggetto Thread che ci permetterà di crearlo. Private _ThreadStart As ThreadStart
Private _Thread As Thread
Lanciare un thread
Come detto in precedenza è possibile far lavorare su un thread separato porzioni di codice appartenenti ad una Sub Routine oppure una intera classe. Nell’esempio successivo viene lanciato in un Thread separato una Sub Routine ClientGoSend. L’istruzione _ClientThread.Start permetterà di lanciare il Thread come viene mostrato nell’esempio successivo. _ThreadStart = New ThreadStart(AddressOf ClientGoSend)
_Thread = New Thread(_ThreadStart)
_Thread.Start
Public sub ClientGoSend
End sub
Meccanismi di Sincronizzazione
I Thread essendo delle entità separate rispetto al processo chiamante che li ha creati hanno una loro vita, questo vuol dire che mentre il padre effettuerà ad esempio un loop infinito il Thread creato in precedenza prosegue in parallelo la propria attività. Questo meccanismo introduce alla programmazione Asincrona che seppur utile aggiunge delle problematiche di sincronismo alle nostre applicazioni. Il .NET Framework ci mette a disposizione dei metodi nella classe thread che permettono di attendere per un determinato tempo un qualche evento oppure ripartire nell’elaborazione allo scatenare di un altro.
Sleep
L’istruzione Thread.Sleep(100) mette in attesa il thread per un valore in Millisecondi. _ThreadStart = New ThreadStart(AddressOf Send)
_Thread = New Thread(_ThreadStart)
_Thread.Start
Console.WriteLine(“Main” & AppDomain.GetCurrentThreadId())‘del thread Main
Public sub Send
Dim objThread As Thread = Thread.CurrentThread()
objThread.Sleep(100) ‘Mette in attesa il thread per 100 Millisecondi
Console.WriteLine(“Thread” & AppDomain.GetCurrentThreadId()) ‘ID del thread
objThread.Abort ‘Distrugge il Thread
End sub
L’istruzione Console.WriteLine(AppDomain.GetCurrentThreadId()) viene ripetuta per due volte all’interno dell’applicazione perché stamperà a console prima l’ID del processo chiamante e poi l’ID del processo chiamato. Il risultato sarà Main 2574
Thread 2575
Oppure
Thread 2575
Main 2574
Questo perchè potrà eseguire l’istruzione Console prima il Main oppure prima il Thread creato. Di seguito vengono riportati dei metodi della classe thread che servono per la sincronizzazione dei Thread.
Start
L’istruzione Start lancia un thread.
ResetAbort
L’istruzione ResetAbort interrompe la distruzione di un thread.
Suspend
L’istruzione Suspend mette in attesa il thread.
Resume
L’istruzione Resume riattiva il thread.
Join
L’istruzione Join mette in attesa il thread. Nell’esempio sotto riportato, il codice Main chiama un thread che terminirà allo scadere di 4 secondi e scriverà “Worker End”. Imports System.Threading
Module Esempio01
Private _ThreadStart As ThreadStart
Private _Thread As Thread
Public Sub Worker()
Thread.Sleep(4000)
Console.WriteLine("Worker End")
End Sub
Sub Main()
_ThreadStart = New ThreadStart(AddressOf Worker)
_Thread = New Thread(_ThreadStart)
_Thread.Start()
_Thread.Join() 'Attende che il thread sia completato
Console.WriteLine("Main End")
End Sub
End Module
Eseguendo il codice vedremo che il risultato sarà il seguente Worker End
Main End
Questo perché il main attenderà il completamento del thread _Thread prima di effettuare la scrittura di “Main End” grazie al metodo Join. Se asterischiamo l’istruzione _Thread.Join() e rieseguiamo l’esempio noteremo che il risultato sarà il seguente. Main End
Worker End
Questo perché il Main non attenderà la conclusione del Thread _Thread che scriverà dopo 4 secondo il messaggio “Worker End”
Il metodo Join può prendere in input un parametro espresso in millisecondi/Timestamp. Tali millisecondi/Timestamp stanno ad indicare il tempo massimo che verrà atteso per la terminazione del Thread.
Interrupt L’istruzione Interrupt interrompe lo stato di attesa di un thread.
Abort L’istruzione Abort distrugge il thread. E’ da notare che tale istruzione non è una garanzia di distruzione in quanto genera al suo interno un’eccezione di tipo ThreadAbortException e imposta lo stato su AbortRequested. Se all’interno di questa eccezione venisse invocato il metodo ResetAbort verrebbe vanificata la chiamata di Abort precedentemente invocata e quindi è importante fare attenzione al codice che stiamo implementando tenendo conto di questi meccanismi di concorrenza. La stessa cosa vale per il metodo Interrupt che a sua volta genera un’eccezione di tipo ThreadInterruptedException e imposta lo stato su Running anche se interromperà l’esecuzione. Il codice seguente mostra il comportamento di quanto descritto sopra.
Imports System.Threading
Module Esempio02
Private _ThreadStart As ThreadStart
Private _Thread As Thread
Sub Main()
_ThreadStart = New ThreadStart(AddressOf Worker)
_Thread = New Thread(_ThreadStart)
_Thread.Start()
_Thread.Abort() 'Da provare cambiandolo in Interrupt
Console.WriteLine("Main End")
End Sub
Public Sub Worker()
Dim objThread As Thread = Thread.CurrentThread()
Try
Console.WriteLine("Worker Sleeping")
Thread.Sleep(4000)
'generata se invocato il metodo interrupt
Catch e As ThreadInterruptedException
Console.WriteLine("Worker interrupt : ThreadState " & _
Thread.CurrentThread.ThreadState.ToString)
'generata se invocato il metodo Abort
Catch e As ThreadAbortException
Console.WriteLine("Worker Abort : ThreadState " & _
Thread.CurrentThread.ThreadState.ToString)
'operazioni conclusive del costrutto Try/Catch
Finally
Console.WriteLine("Worker End : ThreadState " & _
Thread.CurrentThread.ThreadState.ToString)
End Try
Console.WriteLine("Worker Completed")
End Sub
End Module
Eseguendolo il risultato sarà il seguente Worker Sleeping
Worker Abort : ThreadState AbortRequested
Worker End : ThreadState AbortRequested
Main End
Da notare che il thread non è stato completato ( non viene notificato “Worker Completed”) in quanto è stata richiesta una operazione di Abort che viene anche notificata dall’istruzione Thread.CurrentThread.ThreadState.ToString che ci mostra lo stato del thread impostato su AbortRequested. Se al posto di _ClientThread.Abort() inserissimo _ClientThread.Interrupt() nel main del modulo il risultato cambierà in questo modo. Worker Sleeping
Main End
Worker interrupt : ThreadState Running
Worker End : ThreadState Running
Worker Completed
Da notare che il motodo Interrupt ha interrotto l’esecuzione del thread senza distruggerlo, infatti in questo esempio viene notificata la terminazione del thread dal messaggio “Worker Completed” che ci avverte che l’invocazione di interrupt ha interrotto lo stato di Sleep del thread.
SpinWait
L’istruzione SpinWait viene utilizzata su applicazione che girano su macchine multiprocessore e costituisce un’alternativa al comando Sleep che restituisce cicli di CPU rimanendo in attesa.
Stato dei thread I Thread possono assumere diversi stati in funzione della loro sincronizzazione con i processi chiamanti. In questa sezione andremo ad analizzare i diversi stati per capirne meglio il loro utilizzo. Come visto in precedenza, per acquisire lo stato di un thread occorre accedere al metodo Shared Thread.CurrentThread.ThreadState.ToString che ci ritorna un valore in formato Stringa dello stato corrente del thread. Running
La parola stessa ci dice che il thread sta in esecuzione
Imports System.Threading
Module Esempio03
Sub Main()
Dim _ThreadStart As New ThreadStart(AddressOf Worker)
Dim _Thread As New Thread(_ThreadStart)
_Thread.Start()
While True
Thread.Sleep(1000)
Console.WriteLine("Worker End : ThreadState " & _
_Thread.ThreadState.ToString)
End While
Console.WriteLine("Main End")
End Sub
Public Sub Worker()
While True
End While
Console.WriteLine("Worker Completed")
End Sub
End Module
WaitSleepJoin
Questo stato ci comunica che il thread si trova in uno stato di
Wait, Sleep o Join.
Imports System.Threading
Module Esempio04
Sub Main()
Dim _ThreadStart As New ThreadStart(AddressOf Worker)
Dim _Thread As New Thread(_ThreadStart)
_Thread.Start()
While True
Thread.Sleep(1000)
Console.WriteLine("Worker End : ThreadState " & _
_Thread.ThreadState.ToString)
End While
Console.WriteLine("Main End")
End Sub
Public Sub Worker()
While True
Thread.Sleep(1000)
End While
Console.WriteLine("Worker Completed")
End Sub
End Module
In questo caso è lo stesso Thread che sta effettuando un ciclo While
true ogni un secondo.
AbortRequested
La parola stessa ci dice che per il thread è stata invocata una
Abort.
Per il metodo Abort vi rimando all’esempio del metodo Abort nella
sezione Meccanismi di Sincronizzazione
La priorità dei thread
In un ambiente Multi Threading è possibile impostare una priorità di esecuzione per ogni singolo thread. Tale meccanismo permette di condividere nel migliore dei modi i cicli di CPU della macchina permettendo ai processi critici di avere una maggiore priorità in termine di esecuzione sui processi secondari o di minor importanza applicativa. I processi Microsoft .NET sono di due tipi, processi di Foreground e processi di Background. Ai processi di Foreground possiamo attribuire una priorità di esecuzione in base ad una Enumerazione contenuta nella classe System.threading.ThreadPriority qua sotto ricreata per maggiore chiarezza.
‘Enumeration che riporta le priorità per i thread di Foreground
Public Enum Priority
Lowest = 0
BelowNormal = 1
Normal = 2
AboveNormal = 3
Highest = 4
End Enum
Nell’esempio successivo abbiamo utilizzato il metodo Priority per assegnare una priorità di esecuzione. E’ importante dire che un Thread creato dal Runtime senza specifica di priorità viene creato come Foreground di tipo Normal.
Imports System.Threading
Module Esempio05
Sub Main()
Dim _ThreadStart As New ThreadStart(AddressOf Worker)
Dim _Thread As New Thread(_ThreadStart)
_Thread.Start()
_Thread.Priority = ThreadPriority.Lowest
Console.WriteLine("Main End")
End Sub
Public Sub Worker()
Thread.Sleep(4000)
Console.WriteLine("Worker Completed, Priority is : " & _
Thread.CurrentThread.Priority.ToString)
End Sub
End Module
Il risultato di questo esempio ritornerà il seguente Output.
Worker Completed, Priority is : Lowest
Main End
Cambiando il tipo di Priorità da Lowest a Highest noteremo che l’applicazione attenderà sempre la conclusione del thread prima di essere terminata (Ovviamente omettendo il metodo Join). Questo perché si tratta di un thread di tipo Foreground e quindi deve essere per forza terminato prima di terminare l’intero processo. E’ possibile definire un Thread come processo di Background utilizzando la proprietà Thread.IsBackground di tipo Boleano. Impostando a True questo valore il processo potrà essere terminato in qualsiasi momento, talei processi di background saranno processi non di vitale importanza per le nostre applicazioni.
Imports System.Threading
Module Esempio06
Sub Main()
Dim _ThreadStart As New ThreadStart(AddressOf Worker)
Dim _Thread As New Thread(_ThreadStart)
_Thread.Start()
_Thread.IsBackground = True
Console.WriteLine("Main End")
End Sub
Public Sub Worker()
Thread.Sleep(4000)
Console.WriteLine("Worker Completed, Priority is : " & _
Thread.CurrentThread.Priority.ToString)
End Sub
End Module
Il risultato di questo esempio ritornerà il seguente Output.
Main End
Questo perchè il processo figlio è stato definito di Background e quindi è stato terminato senza preavviso dal processo chiamante.
Scopo delle variabili
E’ possibile creare delle variabili che avranno uno scopo limitato al processo che le sta eseguendo. Nell’esempio che viene riportato in seguito, la variabile di classe Value viene dichiarata inserendo il suffisso <ThreadStatic()>. Questa variabile non sarà più una variabile condivisa sia dal processo chiamante che dal processo figlio, ma ogni processo avrà una copia di tale variabile e ne potrà fare un uso esclusivo.
Imports System.Threading
Module Esempio07
<ThreadStatic()> Public Value As String = "Main"
Sub Main()
Console.WriteLine("Main Value = " & Value)
Dim _ThreadStart As New ThreadStart(AddressOf Worker)
Dim _Thread As New Thread(_ThreadStart)
_Thread.Start()
_Thread.Join()
Console.WriteLine("Main Value = " & Value)
End Sub
Public Sub Worker()
Console.WriteLine("Worker Value = " & Value)
Value = "Worker"
Console.WriteLine("Worker Value = " & Value)
End Sub
End Module
Il risultato di questo esempio ritornerà il seguente Output.
Main Value = Main
Worker Value =
Worker Value = Worker
Main Value = Main
Main End
Vediamo come la variabile Value impostata a “Main” sarà visibile solamente nel Main del programma, mentre il Thread Worker ne potrà fare un uso esclusivo impostando un valore che non verrà condiviso con il programma Main chiamante. E’ possibile utilizzando I Thread definire dei punti di codice nel quale non possono accedervi più Thread contemporaneamente. Questo è possibile grazie all’istruzione SyncLock. Nell’esempio successivo verrà definita una variabile di classe di nome Lock e nel metodo Worker verrà utilizzata l’istruzione SyncLock Lock che tramite l’oggetto globale Lock determinerà chi far accedere alla porzione di codice specifica.
L’oggetto Lock deve essere definito di tipo Object.
Imports System.Threading
Module Esempio15
Public Counter As Integer
Public Lock As New Object
Sub Main()
Dim i As Integer
For i = 0 To 10
Dim _ThreadStart As New ThreadStart(AddressOf Worker)
Dim _Thread As New Thread(_ThreadStart)
_Thread.Start()
Next
End Sub
Public Sub Worker()
‘SyncLock Lock
Counter = Counter + 1
Thread.Sleep(New Random().Next(500, 2000))
Console.WriteLine("Worker Counter : " & Counter)
‘End SyncLock
End Sub
End Module
Il risultato di questo esempio ritornerà il seguente Output. Questo perché la display del contatore avverrà dopo che tutti i thread avranno effettuato la somma.
Worker Counter : 11
Worker Counter : 11
Worker Counter : 11
Worker Counter : 11
Worker Counter : 11
Worker Counter : 11
Worker Counter : 11
Worker Counter : 11
Worker Counter : 11
Worker Counter : 11
Worker Counter : 11
Se disasterischiamo le istruzioni SyncLock e rieseguiamo l’esempio vedremo che il risultato cambierà in questo modo.
Worker Counter : 1
Worker Counter : 2
Worker Counter : 3
Worker Counter : 4
Worker Counter : 5
Worker Counter : 6
Worker Counter : 7
Worker Counter : 8
Worker Counter : 9
Worker Counter : 10
Worker Counter : 11
Questo perché il blocco di codice ha un lock determinato dall’oggetto Lock definito come variabile di classe che ne restituisce il sincronismo. Da notare che il primo thread accederà al blocco e gli altri rimarranno in attesa fino a quando il primo thread non accederà all’istruzione End SyncLock, a quel punto il secondo entrerà in azione e così via.
Pool di Thread I Pool di Thread vengono in nostro aiuto quando occorre creare dei processi di lunga durata che lavorano in parallelo. Un classico esempio sono quelle applicazioni Server (Web Server, SMTP Server ecc..) che non possono smaltire una richiesta alla volta ma debbono prendere in carico le richieste e processarle in parallelo. Quindi l’utilizzo di Pool di Thread risulta complesso e oneroso in termine di carico di CPU e quindi se non espressamente richiesto ne faremo volentieri a meno. A nostro aiuto viene la classe ThreadPool che ci consente di creare Thread tramite l’ausilio del metodo QueueUserWorkItem. Tutti i Thread creati da un ThreadPool vengono avviati con Priority di Background.
Imports System.Threading
Module Esempio08
Sub Main()
Dim i As Integer
For i = 0 To 10
ThreadPool.QueueUserWorkItem( _
New WaitCallback(AddressOf Worker), i)
Next
Console.WriteLine("Main id : " & _
AppDomain.GetCurrentThreadId())
Thread.Sleep(5000)
End Sub
Public Sub Worker(ByVal IndexThread As Object)
Console.WriteLine("Thread N° " & IndexThread & " id : " & _
AppDomain.GetCurrentThreadId())
End Sub
End Module
In questo esempio vengono creati tramite la classe ThreadPool dei thread di tipo Worker che verranno eseguiti parallelamente. Il risultato prodotto sarà il seguente.
Main id : 2008
Thread N° 0 id : 532
Thread N° 1 id : 532
Thread N° 2 id : 532
Thread N° 3 id : 532
Thread N° 4 id : 532
Thread N° 5 id : 532
Thread N° 6 id : 532
Thread N° 7 id : 532
Thread N° 8 id : 532
Thread N° 9 id : 532
Thread N° 10 id : 532
La cosa che ci viene subito all’occhio è che l’ID dei thread lanciati sono sempre gli stessi. Questo perché appena verrà accodato (i = 0) al ThreadPool una nuova richiesta, il ThreadPool creerà un nuovo Thread e lo eseguirà. Se in un tempo relativamente breve il Thread verrà terminato, il ThreadPool riutilizzerà lo stesso Thread per l’elaborazione della coda successiva (i=1 , i=2, i=3. i=4 ecc..) Quindi l’ID dei Thread processati sarà sempre lo stesso in quanto verrà riutilizzata la stessa allocazione di memoria e quindi non verrà effettuato un vero parallelismo di applicazione. Ovviamente assumere che il termine dei Thread avvenga in modo lineare è estremamente pericoloso (N°1, N°2, N°3, N°4, N°5, N°6, N°7, N°8, N°9, N°10) Il codice successivo invece mostra un vero parallelismo applicativo in quanto il termine dei Thread creati dal ThreadPool avrà un tempo di durata maggiore del tempo minimo di esecuzione di un Thread. Questo permetterà al ThreadPool di creare Thread con ID differenti e farli lavorare in parallelo. L’istruzione Thread.Sleep(New Random().Next(2000)) ci permette di simulare una durata casuale della funzione Worker.
Imports System.Threading
Module Esempio09
Sub Main()
Dim i As Integer
For i = 0 To 10
ThreadPool.QueueUserWorkItem( _
New WaitCallback(AddressOf Worker), i)
Next
Console.WriteLine("Main id : " & AppDomain.GetCurrentThreadId())
Thread.Sleep(5000)
End Sub
Public Sub Worker(ByVal IndexThread As Object)
Thread.Sleep(New Random().Next(2000))
Console.WriteLine("Thread N° " & IndexThread & " id : " & _
AppDomain.GetCurrentThreadId())
End Sub
End Module
Il risultato prodotto sarà il seguente.
Thread N° 1 id : 3356
Thread N° 0 id : 3644
Thread N° 4 id : 3644
Thread N° 7 id : 1256
Thread N° 2 id : 540
Thread N° 3 id : 3356
Thread N° 6 id : 3644
Thread N° 9 id : 540
Thread N° 10 id : 3356
Thread N° 5 id : 2968
Thread N° 8 id : 1256
Ovviamente alcuni Thread potranno essere riutilizzati dal ThreadPool e quindi manterranno ID analoghi. Il ThreadPool ha dei metodi che possono essere invocati per facilitare il suo utilizzo. Di default il numero massimo di esecuzione parallela è impostato a 25 Thread che tramite il metodo SetMaxThreads può essere variato.
Dim MaxThread As Boolean = ThreadPool.SetMaxThreads(100, 1)
restituisce un valore di tipo Boleano se la richiesta è andata a buon fine. Tale metodo prende in input due valori, Workerthreads che definisce il numero massimo di thread presenti nel Pool e completionPortThreads che prende in input un numero corrispondente alle CPU sulle quali si vuole far girare il Pool. Ovviamente una volta terminato il numero di Thread massimi in esecuzione nel Pool, le richieste rimarranno in sospeso fino al liberarsi di un Thread appartenente al Pool. L’esempio successivo invece utilizza il metodo SetMaxThreads e GetAvailableThreads che ritorna il numero dei thread disponibili all’interno del Pool. Tale valore è la sottrazione tra il numero massimo dei Thread impostati tramite SetMaxThreads e il numero dei thread in esecuzione.
Imports System.Threading
Module Esempio10
Sub Main()
Dim i As Integer
Dim MaxThread As Boolean = ThreadPool.SetMaxThreads(10, 1)
Console.WriteLine("SetMaxThreads : " & MaxThread)
For i = 0 To 10
Dim ReturnAddTopool As Boolean = _
ThreadPool.QueueUserWorkItem( _
New WaitCallback(AddressOf Worker), i)
Next
Thread.Sleep(5000)
End Sub
Public Sub Worker(ByVal IndexThread As Object)
Thread.Sleep(New Random().Next(2000))
Dim WorkerThreads As Integer
Dim ThreadId As Integer = AppDomain.GetCurrentThreadId()
Dim CompletionPortThreads As Integer
ThreadPool.GetAvailableThreads(WorkerThreads, _
CompletionPortThreads)
Console.WriteLine("Thread Available is : " & _
WorkerThreads)
Console.WriteLine("Thread N° " & IndexThread & " id : " & _
ThreadId)
End Sub
End Module
Il risultato sarà il seguente. SetMaxThreads : True
Thread Available is : 9
Thread N° 0 id : 2064
Thread Available is : 9
Thread N° 1 id : 2064
Thread Available is : 7
Thread N° 3 id : 3784
Thread Available is : 6
Thread N° 5 id : 3784
Thread Available is : 6
Thread N° 2 id : 2064
Thread Available is : 5
Thread N° 9 id : 3596
Thread Available is : 5
Thread N° 8 id : 2064
Thread Available is : 6
Thread N° 4 id : 3320
Thread Available is : 7
Thread N° 7 id : 3784
Thread Available is : 8
Thread N° 6 id : 344
Thread Available is : 9
Thread N° 10 id : 3596
Come vediamo Thread Available è variabile in base a diversi fattori, la cosa che salta subito all’occhio e che ci saremmo aspettati dalle print del 2 thread che i Thread disponibili siano minori di 9. Come visto in precedenza il ThreadPool definisce un tempo entro il quale allocare una seconda risorsa per attivare un nuovo Thread. Se la precedente richiesta finisce entro un tempo relativamente breve riutilizza la stessa (e per questo mantiene anche lo stesso ID di processo).
Gli eventi
Fino ad ora abbiamo creato un Thread passandogli un indirizzo di una Sub Routine dichiarata all’interno della funzione Main(), come detto in precedenza è possibile istanziare una classe, passargli dei parametri e poi eseguire in un thread separato una Routine presente di quella classe. Nell’esempio successivo viene dichiarata una classe che al suo interno dispone di un evento TerminateThread che verrà generato per segnalare al Main() il completamento delle operazioni.
Imports System.Threading
Module Esempio11
Public WithEvents _Worker As Worker
Sub Main()
_Worker = New Worker
_Worker.TimeSleep = 2000
Dim _ThreadStart As New ThreadStart(AddressOf _Worker.Start)
Dim _Thread As New Thread(_ThreadStart)
_Thread.Start()
Console.WriteLine("Main End")
End Sub
Private Sub _Worker_TerminateThread(ByVal Result As String) _
Handles _Worker.TerminateThread
Console.WriteLine(Result)
End Sub
End Module
Public Class Worker
Public Event TerminateThread(ByVal Result As String)
Public TimeSleep As Integer
Public Sub Start()
Thread.Sleep(TimeSleep)
RaiseEvent TerminateThread("Worker Completing. TimeSleep is : " & _
TimeSleep)
End Sub
End Class
I risultato dell’elaborazione è il seguente
Main End
Worker Completing. TimeSleep is : 2000
Questo meccanismo di segnali ci permetterà di avere un controllo preciso su ciò che sta succedendo all’interno del Thread. Ma è possibile anche mettere in attesa un Thread fino a che non si verifichi una condizione per cui far continuare l’elaborazione senza utilizzare i comandi di sincronizzazione Suspend e poi il relativo Resume.
Nel codice successivo utilizzeremo il comando AutoResetEvent che ci permette di utilizzare i metodi WaitOne() e Set() con cui mettere in attesa un Thread (WaitOne) e poi successivamente dal Main far continuare l’elaborazione (Set). Oltre a WaitOne() è possibile mettere in attesa il Thread tramite WaitAny() e WaitAll(). WaitAny() mette in attesa il Thread fino a quando un evento venga segnalato. WaitAll() invece mette in attesa il Thread fino a che tutti gli eventi vengano segnalati. Ovviamente l’evento viene segnalato quando viene evocato il metodo Set(). Questo genere di meccanismi ci permettono di controllare perfettamente il codice che viene processato all’interno dei Thread.
Imports System.Threading
Module Esempio12
Public _AutoResetEvent As AutoResetEvent
Sub Main()
_AutoResetEvent = New AutoResetEvent(False)
Dim _ThreadStart As New ThreadStart(AddressOf Worker)
Dim _Thread As New Thread(_ThreadStart)
Console.WriteLine("Main : Start Thread")
_Thread.Start()
Console.WriteLine("Main Slepp 1 Second...")
Thread.Sleep(1000)
Console.WriteLine("Main Signal Worker Thread")
_AutoResetEvent.Set()
End Sub
Public Sub Worker()
Console.WriteLine("Worker thread Active")
_AutoResetEvent.WaitOne()
Console.WriteLine("Worker thread Completing")
End Sub
End Module
I risultato dell’elaborazione è il seguente
Main : Start Thread
Worker thread Active
Main Slepp 1 Second...
Main Signal Worker Thread
Worker thread Completing
I Timer Il nameSpace System.Threading comprende un oggetto molto utile che andremo ad analizzare che si chiama Timer. Il timer viene in nostro aiuto quando occorre far eseguire porzioni di codice a tempo. Nell’esempio successivo viene mostrato l’utilizzo di un timer, da tenere in considerazione che questo tipo di classe può essere utilizzata anche all’interno dei Windows Service quando non è possibile utilizzare la classe Timer presente nelle Windows Form. Il costruttore della classe Timer prende in input un oggetto di tipo TimerCallBack che identifica la locazione di memoria da andare ad eseguire, lo State, un oggetto contenente i valori da passare al metodo da eseguire, dueTime che specifica il tempo che verrà calcolato prima della prima chiamata (0 vuol dire subito) e infine il Period che corrisponde al tempo in millisecondi tra una chiamata e l’altra. Imports System.Threading
Module Esempio13
Sub Main()
Dim _TimerCallback As New TimerCallback(AddressOf Worker)
Dim _Timer As Timer = New Timer(_TimerCallback, _
Nothing, _
0, _
1000)
Thread.Sleep(6000)
End Sub
Public Sub Worker(ByVal State As Object)
Console.WriteLine("Worker Now : " & DateTime.Now.ToLongTimeString())
End Sub
End Module
Il risultato sarà il seguente.
Worker Now : 9.27.43
Worker Now : 9.27.44
Worker Now : 9.27.45
Worker Now : 9.27.46
Worker Now : 9.27.47
Worker Now : 9.27.48
Worker Now : 9.27.49
Apartment Thread e interoperabilità COM
In ultima analisi vediamo in che maniera far collaborare i Thread .NET con il vecchio mondo COM. Per questo motivo avremo senz’altro notato delle proprietà presenti all’interno dell’oggetto Thread denominate SetApartmentState() e GetApartmentState(). L’Apartment State è un contenitore logico presente in un processo dove sono allocati oggetti che condividono gli stessi requisiti di accesso ad un Thread. Tele specifica esiste nella vecchia COM ma non è presente all’interno del Common Language Runtime di .NET. I Componenti COM grazie all’Apartment garantivano una corretta sincronizzazione delle risorse e per questa loro logica di implementazione che occorre modificare i nostri Thread se vogliamo far uso di componenti COM dal loro interno. La proprietà SetApartmentState() prende in input i valori dati dall’enumerazione riportata in seguito per comodità.
Public Enum ApartmentState
STA = 0 'Single thread Apartment
MTA = 1 'MultiThread Apartment
unknow = 2 'Non definito
End Enum
Se non viene dichiarato un ApartmentState() lo stato prelevato dalla proprietà GetApartmentState() sarà impostato su unknow. l’oggetto COM se noterà un Apartment incompatibile utilizzerà un proxy per farlo lavorare correttamente. Per migliorare le performance di comunicazione tra il thread chiamante e l’oggetto COM è meglio specificare il corretto stato di Apartment da utilizzare. Non è possibile modificare una volta avviato il Thread lo stato di Apartment ne annullarne l’inizzializzazione. Imports System.Threading
Imports ComDemo
Module Esempio14
Sub Main()
Dim _ThreadStart As New ThreadStart(AddressOf Worker)
Dim _Thread As New Thread(_ThreadStart)
Try
Console.WriteLine("Worker ApartmentState : " & _
_Thread.GetApartmentState.ToString)
_Thread.TrySetApartmentState(ApartmentState.STA)
Console.WriteLine("Worker ApartmentState : " & _
_Thread.GetApartmentState.ToString)
_Thread.Start()
Catch ex As ThreadStateException
Console.WriteLine("ThreadStateException occurs " & ex.Message)
End Try
_Thread.Join()
End Sub
Sub Worker()
Dim ComObject As New ComDemo.Class1
Console.WriteLine("Worker invoke SetValue")
ComObject.SetValue("Inizialize")
Thread.Sleep(2000)
Dim ComValue As String = ComObject.GetValue
Console.WriteLine("Worker invoke GetValue : " & ComValue)
End Sub
End Module
Il risultato sarà il seguente.
Worker ApartmentState : Unknown
Worker ApartmentState : MTA
Worker invoke SetValue
Worker invoke GetValue : Inizialize
Questo esempio fa vedere che è possible impostare l’ApartmentState solamente in fase di creazione del thread prima di aver invocato il metodo Start().
Conclusioni
Il MultiThreading in Microsoft .NET è possibile ed è di semplice utilizzo come abbiamo appreso in questo documento. Le tecniche di Pool di thread già presenti all’interno del Framework facilitano di gran lunga la realizzazione e la progettazione di applicativi scalabili e performanti che fino ad ora erano onerosi e complessi da realizzare. Al programmatore è rivolta la massima accortezza nello scrivere le applicazioni che con l’avvento del MultiThreading introducono meccanismi complessi di tracciamento dei processi e di Locking.
Bibliografia