Il .NET Framework fornisce la possibilità di creare applicativi multithreaded. In questo articolo viene spiegato il concetto di Multithreading e le tecniche per poter gestire i thread in maniera efficace e semplice. In particolare, vedremo come eseguire una procedura in un thread separato aggiungendo questo thread ad un thread pool.
Cos'è il Multithreading
Il Multithreading è quella caratteristica (tipica dei sistemi operativi) che permette ad una applicazione di eseguire una o più azioni (thread) in parallelo.
Nel caso specifico è utile se vogliamo evitare che il thread corrispondente ad una richiesta, venga bloccato finché questa venga portata a termine. In una applicazione "Multithreaded" possiamo svolgere più attività (o task) contemporaneamente: l'azione successiva può essere svolta senza dover attendere che quella precedente venga completata. Ad esempio nei programmi di elaborazione testi (come Word), mentre digitiamo il testo viene svolto anche il controllo dell'ortografia e il salvataggio del documento ad intervalli regolari di tempo.
Per comprenderne il funzionamento esaminiamo un esempio con un singolo thread.
Listato 1. Codice che non sfrutta il multithreading
Class _Default Inherits System.Web.UI.Page
Public Sub Sub1()
Dim i As Integer
For i = 1 To 5
Response.Write("Sub1() </br>")
Next
End Sub
Public Sub Sub2()
Dim i As Integer
For i = 6 To 10
Response.Write("Sub2() </br>")
Next
End Sub
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Sub1()
Sub2()
Response.Write("</br> ---- FINE ----")
End Sub
End Class
La Sub2()
viene eseguita solamente quando Sub1()
ha completato la sua esecuzione. Questo perchè quando una procedura o una funzione viene richiamata il controllo dell'esecuzione viene trasferito all'interno della funzione (o procedura).
Quando termina l'esecuzione della procedura, l'applicazione riparte dalla linea di codice successiva alla linea che ha richiamato la procedura.
Utilizzando il Multithreading, possiamo eseguire due thread concorrenti (in parallelo) all'interno della stessa applicazione. Questa tecnica diventa indispensabile quando si eseguono operazioni molto lunghe e laboriose che richiedono elevati carichi di lavoro come:
- query complesse su database
- operazioni di Input/Output
- chiamate a web service remoti
Se una di queste operazioni, impiega un intervallo di tempo che viene considerato inaccettabile, è buona regola ricorrere al multithreading, altrimenti il thread corrispondente alla richiesta, viene bloccato fin quando la medesima richiesta non viene portata a termine.
In questo modo, l'interfaccia della nostra applicazione non viene bloccata e si possono quindi eseguire altre operazioni, mentre la funzione che richiede tempi di esecuzione lunghi continua ad essere eseguita in background.
È importante ricordare che bisogna prestare la massima attenzione quando si lavora con il multithreading perché in questo tipo di applicazione i bug sono difficili da intercettare e la creazione e la distruzione dei thread possono anche decrementare le performance della nostra applicazione se sono mal gestiti.
Il Thread Pool
Il .NET Framework fornisce la possibilità di creare applicativi multithreaded e, attraverso IIS (Internet Information Service), rende disponibile ad ogni applicazione ASP.NET un thread pool.
Per evitare la creazione di un thread per ogni singola richiesta con il thread pool si utilizza un insieme finito di thread da cui prelevare i vari thread per eseguire le richieste.
Quindi, non è necessario creare un nuovo thread, perché quando ci occorre, la nostra applicazione ne otterrà uno dal thread pool.
Non appena il thread completa la sua esecuzione verrà restituito al thread pool in attesa di essere riutilizzato per una nuova operazione, anziché essere distrutto.
Questo riutilizzo rende la nostra applicazione più performante e riduce il costo di un eccessiva creazione e distruzione di thread.
Un esempio pratico
Eseguiamo in un thread separato del thread pool, un'operazione potenzialmente lunga. Creiamo una nuova applicazione web e una pagina che chiamiamo "default.aspx".
La prima cosa da fare è includere il namespace System.Threading
per poter creare i nuovi thread, mentre il namespace System.IO
ci occorre per poter effettuare le operazioni di lettura/scrittura su file.
Imports System.Threading
Imports System.IO
Ora creiamo la procedura che verrà eseguita in background in un thread separato visto che potrebbe impegnare molta memoria.
Listato 2. Procedura "impegnativa"
Private Sub Scrivi100MilaRighe(ByVal s As Object)
Dim i As Integer
Dim sw As StreamWriter = New StreamWriter("c:taskNuovo.txt")
For i = 0 To 100000
sw.WriteLine("questa è una riga" & " - Data: " + DateTime.Now.ToString() & _
" millisecondi:" & DateTime.Now.Millisecond.ToString())
Next
sw.WriteLine(" - FINE : " + DateTime.Now.ToString() & " millisecondi:" & DateTime.Now.Millisecond.ToString())
sw.Close()
End Sub
Trasciniamo un pulsante nella nostra pagina e lo chiamiamo "Button1" e sul click del pulsante aggiungiamo il codice che lancia l'esecuzione della procedura Scrivi1000MilaRighe()
in un thread del pool e poi stampa a video un messaggio.
Listato 2. Pagina multithread
Class _Default Inherits System.Web.UI.Page
Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
If ThreadPool.QueueUserWorkItem(AddressOf Scrivi100MilaRighe) Then
Label2.Text = " Accodato nel thread pool </br>" & _
DateTime.Now.ToString() & " millisecondi:" & DateTime.Now.Millisecond.ToString()
Else
Label2.Text = "Si è verificato un errore, impossibile accodare nel thread pool"
End If
End Sub
End Class
Abbiamo utilizzato la funzione ThreadPool.QueueUserWorkItem
per eseguire la nostra procedura in un thread del thread pool. Il parametro passato alla funzione è la funzione che deve essere eseguita. Nel nostro caso è Scrivi100MilaRighe
. La funzione viene eseguita quando un thread del pool di thread diventa disponibile. ThreadPool.QueueUserWorkItem
restituisce il valore true se l'operazione di accodamento ha avuto successo.
Il thread è stato accodato nel thread pool ed è stato eseguito.
Nota: nel nostro esempio abbiamo utilizzato semplicemente l'operatore AddressOf
per passare il metodo di callback a QueueUserWorkItem
omettendo il costruttore WaitCallback
perché Visual Basic chiama automaticamente il costruttore delegato corretto.
Il delegato WaitCallback
rappresenta infatti il metodo di callback che deve essere eseguito da un thread del pool .