I sistemi operativi moderni moderni permettono l'esecuzione contemporanea (quasi!!) di
più applicazioni e per questo vengono detti Multitask. Una applicazione può quindi
essere eseguita contemporaneamente ad un'altra (per esempio quando navighiamo su internet
e contemporaneamente controlliamo la posta, oppure ricerchiamo un file sul disco). Durante
la sua esecuzione, una applicazione deve bloccare completamente l'esecuzione quando
aspetta che terminino processi lenti come l'accesso ai dischi o la comunicazione con altri
computer. Molte volte le applicazioni possono essere organizzate in maniera tale da
ovviare a questi colli di bottiglia o in maniera da suddividere la loro esecuzione in più
processi, magari ripartiti su più processori in sistemi multiprocessore. Ecco che in
alcuni dei moderni sistemi operativi ci vengono in contro i Thread. In questo caso avremo
a che fare con sistemi operativi multithread. La differenza tra questi due tipi di sistemi
operativi sta nel fatto che nel primo tipo, l'unità elementare di esecuzione è il task
(processo), mentre nel secondo è il thread. Nell'ultimo tipo di sistema operativo,
possiamo avere dei tasks consistenti in più trhead. La differenza fondamentale tra task e
trhead sta nel fatto che i primi non possono condividere lo stesso spazio di
indirizzamento mentre invece i secondi, ovviamente se appartenenti alle stesso task (o
processo), si. La creazione di un nuovo thread non richiede l'impiego di grandi risorse di
sistema. Normalmente in una applicazione si ha un thread definito principale, ed è quello
che gestisce i messaggi di Windows e l'esecuzione del codice principale e nessuno, uno o
più trhead secondari.
La definizione, la creazione e la gestione di thread non è proprio semplice ed
intuitiva. Delphi però, come sempre, ci viene incontro semplificando notevolmente le cose
mettendoci a disposizione la classe TThread. L'uso di questa classe è molto semplice.
Questa contiene alcune proprietà e metodi tra cui il principale è il metodo Execute.
Questo è il metodo il cui codice viene eseguito all'interno del thread. Una volta
definita una nuova classe ereditata da TThread, per la creazione della classe è
necessario effettuare una chiamata al metodo Create della classe la quale ha il seguente
struttura
Create(Suspended : Boolean);
Il parametro Suspended da passare al metodo, permette di definire il comportamento del
thread in seguito alla sua creazione. Impostando questo parametro a False (valore
predefinito) il thread viene attivato immediatamente dopo essere stato creato. Se il
parametro Suspended ha valore true, il thread viene creato e immediatamente sospeso,
ovvero la sua esecuzione non ha inizio fino ad una chiamata al metodo Resume della classe
TThread. Per sospendere di nuovo il thread dopo il suo avvio, è disponibile il metodo
Suspend. Esistono altri metodi che permettono di controllare l'esecuzione del thread come
Terminate che decreta l'arresto definitivo del thread.
I metodi descritti precedentemente sono affiancati da alcune proprietà che permettono
di acquisire informazioni riguardo allo stato di esecuzione del thread. La proprietà
Suspended ci informa sul fatto che il thread è stato sospeso; la proprietà Terminated ci
indica che il thread è stato terminato; Priority permette di impostare la priorità di
esecuzione del thread su 7 livelli differenti di priorità da un minimo di tpIdle ad un
massimo di tpTimeCritical; FreeOnTerminate che permette di specificare se l'oggetto thread
creato deve essere distrutto al termine dell'esecuzione del codice del thread.
Vediamo ora come va implementato il codice da eseguire all'interno del thread vero e
proprio. Quando si definisce una classe erede di TThread, occorre anche riscrivere il
metodo Execute (che nella classe TThread è Virtual e Abstract, ovvero non ha
implementazione) facendo l'ovverride del metodo ereditato. Il codice che andremo a
scrivere all'interno del metodo Execute, sarà il codice che verrà eseguito all'interno
del thread. Ecco come si presenta la struttura del metodo Execute
Type NomeClasse = class(TThread)
private
{ Private declarations }
protected
{ Public declarations }
Procedure Execute; override;
end;
Implementation
Procedure NomeClasse.Execute;
Begin
//Inserire qui il codice da eseguire nel thread
End;
Da notare che il codice del metodo Execute, viene eseguito una volta sola quindi
raggiunta l'ultima istruzione del metodo Execute, l'esecuzione del thread si arresta. Per
eseguire più volte lo stesso codice all'interno del thread, occorre ricorrere ad un ciclo
e, cosa importante, ad ogni esecuzione del ciclo controllare il valore della proprietà
Terminated del thread per interrompere il ciclo quando l'esecuzione del thread viene
interrotta. Una tipica implementazione del metodo Execute è la seguente
Procedure NomeClasse.Execute;
Begin
While not Terminated do
Begin
//Inserire qui il codice da eseguire nel thread
End;
End;
Una importante considerazione da fare riguardo al metodo Execute, sta nel fatto che non
si può eseguire codice che fa riferimento ad oggetti della VCL. Questo perchè gli
oggetti della VCL vengono eseguiti in un thread differente da quello in esecuzione
all'interno del metodo Execute e quindi si potrebbero avere problemi di accesso alle loro
proprietà ed ai lor metodi. Per evitare ciò, la classe TThread mette a disposizione il
metodo Synchronize che permette appunto di sincronizzare i due threads in maniera tale da
paermettere l'accesso sicuro alle proprietà ed ai metodi dell'oggetto voluto. Esistono
anche oggetti della VCL chiamati Thread-Safe che sono stati realizzazti appositamente per
l'utilizzo in ambienti multithread e che permettono di gestire gli accessi al loro
contenuto in maniera concorrente. Un esempio di questi oggetti è la ThreadList.
Al termine dell'esecuzione del codice thread, è possibile eseguire del codice di
pulizia scrivendo un gestore di evento per l'unico evento che mette a disposizione la
classe TThread, l'evento OnTerminate.
La classe TThread permette anche di sincronizzare l'esecuzione del suo thread con altri
thread. tramite il metodo WaitFor della classe TThread è possibile attendere il termine
dell'esecuzione di un'altro thread. Questo metodo non restituisce il controllo finchè
l'altro thread non è terminato.
Potrebbe capitare la necessità di intercettare quando un thread ha terminato una certa
operazione piuttosto che attendere che l'intero thread completi la sua esecuzione. In
questo caso esistono gli oggetti evento (TEvent). Quando un thread deve comunicare ad
altri di aver completato la un certa operazione, chiama il metodo SetEvent dell oggetto
evento. Per disattivare la segnalazione dell'evento è sufficiente effettuare una chiamata
al metodo ResetEvent dell'evento. Gli oggetti evento devono essere creati in un campo
d'azione globale per far si che tutti i thread interessati possano accedervi. Il thread
che necessita di attendere il completamento di una determinata operazione, crea ed avvia i
thread interessati ed effettua una chiamata la metodo WaitFor dell'oggetto evento
indicando un intervallo di tempo da attendere per la ricezione del segnale. Il tempo da
attendere per il settaggio del segnale è espresso in millisecondi e può essere impostato
anche su INFINITE. Al ritorno dalla sua chiamata, il metodo WaitFor restituisce uno dei
seguenti valori: wrSignaled, wrTimeout, wrAbandoned, wrError.
L'oggetto evento, o meglio la classe TEvent, non è altro che una struttura wrapper per
gli oggetti event di Windows.
Altre strutture permettono di semplificare la gestione di multithread. Per esempio,
quando gli oggetti su cui si sta operando non possiedono metodi o proprietà specifiche
per operare in ambineti multithread, si può ricorrere alle sezioni critiche. Le sezioni
critiche permettono di far accedere ai metodi o alle proprietà di un determinato oggetto
un solo thread alla volta, onde evitare conflitti di accesso. Questa struttura è
definita nella classe TCriticalSection che possiede due metodi: Acquire, Release. Quando
un thread deve accedere alla sezione critica, deve prima chiamare il metodo Acquire per
bloccare l'accesso agli altri threads e rilasciare tale blocco tramite il metodo Release
al termine dell'operazione.