Lo posto qui in modo che i piu' esperti possano criticarlo e segnalarmi
eventuali errori, e che i nuovi possano imparare a prendere confidenza con
questi Thread troppo spesso ritenuti troppo complessi per essere usati.
Fatemi sapere che ne pensate :)
{MSX}
Salve a tutti! In questo tutorial parleremo dei Threads di windows e di come
utilizzarli con Delphi. Per prima cosa, vediamo cosa sono i threads. Tutto
cio' che imparerete qui tornera' valido anche per altri ambienti e
linguaggi, come ad esempio Java e Python.
COSA SONO I THREADS
Avrete sentito parlare di applicazioni Multi-threading, o sistemi operativi
Multi-threading e cose simili, immagino. Ma cosa si nasconde dietro tali
altisonanti espressioni? Ebbene, con tali espressioni ci si riferisce a
programmi che sono in grado di eseguire due o piu' porzioni di codice
"contemporaneamente". Un sistema operativo M.T. e' un sistema operativo che
supporta le applicazioni M.T. (ad esempio Windows e BeOS, mentre Linux e' un
po' diverso).
Diciamo che cosi' come il sistema operativo puo' far girare piu' programmi
(processi) contemporaneamente (multitasking), un'applicazione puo' far
girare piu' threads contemporaneamente.
Come potete immaginare, la possibilita' di eseguire due codici
contemporaneamente e' molto utile. E' possibile ad esempio far andare delle
computazioni lunghe in background e permettere comunque all'utente di
lavorare con il programma. I thread sono molto usati anche per programmare
applicazioni client/server, perche' necessitano di un thread che si fermi ad
aspettare i dati da una connessione o cose simili.
In generale tutti i programmi hanno un thread, che e' il thread principale,
che in delphi a volte si trova chiamato come "VCL thread", perche' si occupa
di tutti gli eventi della form e dei controlli della VCL. Quindi se create
un altro thread, ne avrete due, il principale e il secondario che avete
creato voi.
E' possibile che un componente crei al suo interno un Thread che si gestisce
tutto da solo. Quando voi lo usate, vi appare come un normale componente, ma
in realta sta lavorando su un altro thread in modo del tutto trasparente,
cosi' che voi potete usare tale componente senza neanche sapere cosa siano i
thread! E' il potere dell'incapsulazione. Quasi tutti i componenti che hanno
a che fare con Socket e Internet usano dei thread aggiuntivi per leggere e
scrivere i dati.
A meno che non abbiate piu' di un processore, tutti i thread attivi vengono
eseguiti su una singola CPU. Questo significa che in effetti NON sono
eseguiti contemporaneamente, ma si succedono rapidamente sulla CPU, che
esegue quindi un pezzetto alla volta di ciascun Thread. Questo
avvicendamento e' in genere molto rapido, cosi' che noi utenti non ce ne
accorgiamo e diciamo che i thread sono eseguiti "contemporaneamente".
Nel caso abbiate piu' di una CPU, ciascuna di esse puo' eseguire un thread
alla volta. La gestione delle CPU cambia da un sistema operativo a un'altro,
in Windows la regola e' che delle N CPU disponibili, N-1 CPU sono dedicate a
tempo pieno ai N-1 thread con priorita' piu' alta, mentre la rimanente CPU
si accolla il lavoro di tutti i rimanenti thread esistenti sul sistema.
Se aprite il TaskManager su Windows2000 e guardate su "prestazioni" potete
vedere quanti thread sono attivi sulla vostra macchina. Nella mia in questo
momento ce ne sono 296.
I THREADS IN DELPHI
Ovviamente per creare un thread in windows, e' necessario rivolgersi alle
sue api, in particolare alla CreateThread:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to thread security
attributes
DWORD dwStackSize, // initial thread stack size, in bytes
LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function
LPVOID lpParameter, // argument for new thread
DWORD dwCreationFlags, // creation flags
LPDWORD lpThreadId // pointer to returned thread identifier
);
In Delphi invece esiste una classe che incapsula la gestione dei thread in
maniera molto efficiente. La classe in questione e' la TThread.
TThread e' una classe abstract, cioe' non puo' essere usata cosi' com'e', ma
va derivata. In particolare il medoto astratto e' Execute.
Si puo' facilmente intuire l'utilizzo di Execute e perche' sia astratta:
Execute e' la procedura che verra' eseguita "contemporaneamente" al corpro
principale del programma, e ovviamente va riempita col codice che vogliamo
eseguire.
Ecco come si puo' derivare una semplice classe:
type TThreadInutile=class (TThread)
procedure Execute; override;
end;
e poi:
procedure TThreadInutile.Execute;
begin
sleep(INFINITE);
end;
Ecco fatto! In poche righe di codice, abbiamo definito un thread! Questo
thread e' tuttavia completamente inutile, dal momento che non fa altro che
dormire..
Vediamo ora come creare il thread.
var t:TThreadInutile;
begin
t:=TThreadInutile.create(false);
end;
Ecco qui, si crea come qualsiasi altra classe. Vediamo che il parametro da
passare serve a dire se il thread appena creato deve subito essere eseguito,
o se deve essere creato sospeso. Con false viene subito eseguito. E'
tuttavia utile in molti casi crearlo sospeso e poi avviarlo dopo, in modo da
avere il tempo di inzializzare altre cose necessarie al suo funzionamento.
Vediamo ora un esempio piu' complesso, mettiamo che vogliamo avvertire
l'utente del nostro programma ogni volta che passano due ore, in modo che
posso prendersi il quarto d'ora di pausa garantito dalla legge (che esempio,
eh ? :)
In pratica questo Thread non fara' altro che disturbare l'utente con una
finestra di avviso.
type TThreadRompiscatole=class(TThread)
procedure Execute; override;
end;
var
t:TThreadRompiscatole;
implementation
procedure TThreadRompiscatole.Execute;
begin
while true do
begin
sleep(1000*60*60*2); // 2 ore in millisecondi
showmessage('Sono passate due ore! E' tempo di riposare!');
end;
end;
Ora possiamo dire che questo e' quanto c'e' da sapere di base per i thread.
Molto semplice no?
Ad ogni modo c'e' molto altro da imparare.
PROBLEMI DI SINCRONIZZAZIONE:
Il problema principale quando si lavora con i threads, e' la
sincronizzazione. Il fatto e' che il codice dei due thread (quello
principale e il nostro) sono eseguiti contemporaneamente, e non abbiamo
nessuna garanzia dell'ordine in cui vengono eseguite le istruzioni dei due
thread.
Il tipico problema e' quando due o piu' thread tentano di accedere
simultaneamente a un dato.
Ipotizziamo che abbiamo due thread, e che entrambi debbano incrementare una
variabile con l'istruzione
X:=X+1;
ora, questa istruzione si traduce in tre sotto-istruzioni, che sono:
-Evaluta X (legge il contenuto di X dalla memoria)
-Somma X e 1 (esegue la semplice somma)
-Scrivi il risultato su X (scrive sulla memoria)
In generale, se siamo fortunati, le 6 istruzioni si eseguono correttamente:
Thread A - Evaluta
Thread A - Somma
Thread A - Scrivi
Thread B - Evaluta
Thread B - Somma
Thread B - Scrivi
E a questo punto la variabile e' giustamente incrementata di due. Se X
partiva con valore 5, dopo le istruzioni avra' valore 7.
Ma vediamo quest'altro ordine:
Thread A - Evaluta // legge 5
Thread B - Evaluta // legge ancora 5
Thread B - Somma // 5+1=6
Thread B - Scrivi // X diventa 6
Thread A - Somma // Thread A aveva letto 5, quindi ora somma 5+1=6
Thread A - Scrivi // Thread A scrive di nuovo 6!!
Avete visto che roba ? Il thread A ha praticamente sovrascritto il risultato
di B! Ora X contiene 6 invece che 7! In pratica, uno dei due incrementi si
e' perso per strada...
Perche' questa cosa? Perche' entrambi hanno evalutato X all'inizio, non
curandosi del fatto di essere in due a leggerlo. Ricordate sempre che non e'
possibile sapere a priori con quale ordine saranno eseguite le istruzioni:
potrebbero essere interposte in tutti i modi possibili!
E qui sta il vero problema dei thread: lo stesso codice puo' funzionare 100
volte consecutive e poi bloccarsi una volta.. I bug di sincronizzazione sono
difficili da correggere perche' non si ripresentano a comando. E' una vera
dannazione!
Fortunatamente esistono un bel po' di tecniche e strumenti per gestrire
questo tipo di situazioni.
Dovrete pianificare per bene il funzionamento del vostro programma
multithread per evitare di incappare in questi problemi. Come regola,
considerate che qualsiasi dato che potrebbe essere letto o scritto da
entrambi i thread va controllato con le tecniche di sincronizzazione.
SINCRONIZZAZIONE il metodo Synchronize
Il primo aiuto che ci viene fornito, e' il metodo Synchronize della classe
TThread.
Principalmente, questa classe permette di chiamare un metodo dal secondo
thread in modo che venga eseguito nel thread principale.
Ipotizziamo che nella vostra finestra del programma vogliate mettere una
ProgressBar per visualizzare i progressi del thread, che mostri una
percentuale del lavoro fatto.
Ad un primo esame, potreste pensare di fare semplicemente cosi':
procedure TMyThread.Execute;
var percentuale:integer;
begin
percentuale:=0;
while not finito do
begin
// ...
// fai un po' di calcoli
// ...
inc(percentuale);
Form1.ProgressBar1.Position:=percentuale;
end;
end;
Solo che questo significa scrivere un dato (il Position di ProgressBar)
senza sincronizzare. Potrebbe capitare che anche nel thread principale si
tenti di manipolare lo stesso dato! E anche se non lo si fa, e' possibile
che lo faccia la VCL nei suoi metodi interni.
Quindi quando accedete agli oggetti della finestra, dovete sempre
sincronizzare gli accessi anche se non state accedendovi dal thread
principale. Sara' un poco di lavoro in piu', ma vi evitate di aver a che
fare con i terribili bug di sincronizzazione.
Quindi come si fa questa cosa della ProgressBar? Si fa eseguendo
ll'aggiornamento di Position nel thread principale, tramite il metodo
Synchronize:
type TMyThread=class (TThread)
percentuale:integer;
procedure Execute; override;
procedure AggiornaProgressBar;
end;
implementation
procedure TMyThread.AggiornaProgressBar;
begin
Form1.ProgressBar1.Position:=percentuale;
end;
procedure TMyThread.Execute;
begin
percentuale:=0;
while not finito do
begin
// ...
// fai un po' di calcoli
// ...
inc(percentuale);
Synchronize(AggiornaProgressBar); // qui sta il trucco!
end;
end;
Cosa succede praticamente? Succede che quando arriva alla chiamata
Synchronize, il secondo thread si blocca, e la procedura AggiornaProgressBar
viene eseguita dal thread principale, dopodiche' il thread secondario
riparte e continua a lavorare. In questo modo si evita qualsiasi problema di
sovrascrittura.
Il metodo Synchronize prende come parametro un qualsiasi metodo senza
parametri.
CONTROLLARE IL THREAD SECONDARIO DAL PRINCIPALE
Ora, il Synchronize e' sicuramente utile, ma non copre tutte le
possibilita'. In particolare, e' utile per lavorare dal thread secondario
sul primario, ma non viceversa! Se noi vogliamo dal thread primario fare
qualcosa sul secondario, dobbiamo usare tecniche diverse.
Un aiuto puo' essere quello dei metodi Suspend e Resume. Come certamente
avrete capito, servono per "bloccare" un thread in esecuzione e poi a farlo
"ripartire".
NOTA: le chiamate a Suspend si sommano, ed e' necessario un ugual numero di
chiamate Resume per risvegliare il thread addormentato!
Il solito esempio puo' essere un thread molto laborioso, la cui durata puo'
essere di qualche ora, e una finestra che permette di mettere in pausa il
thread quando l'utente debba fare qualche altro lavoro e necessita di CPU.
Il codice di tale pulsante di Pausa, potrebbe essere:
procedure TForm1.Button1Click(Sender: TObject);
begin
if Thread.Suspended then Thread.Resume else Thread.Suspend;
end;
Ovvero lo attiva se e' sospeso, lo sospende se e' attivo.
Anche in questo caso pero' bisogna stare molto attenti: il thread puoi
venire sospeso in qualsiasi punto del suo codice! Si ripresentano cosi' i
problemi di prima.
Ennesimo esempio: vogliamo costruire un rozzo cronometro. Il thread
secondario non fara' altro che incrementare una variabile "tempo", mentre
nel thread principale ci sara' un pulsante per azzerare il timer in modo da
farlo ripartire da zero.
Ecco come verrebbe il thread
type TMyThread=class (TThread)
Tempo:integer;
procedure Execute; override;
end;
procedure TMyThread.Execute;
begin
while true do
inc(tempo);
end;
Mentre nel bottone potrebbe esserci questo codice:
procedure TForm1.Button1Click(Sender: TObject);
begin
Thread.Suspend;
Thread.Tempo:=0;
Thread.Resume;
end;
Questo codice funzionera' nel 99,999% dei casi. Ma sostanzialmente, e'
sbagliato, perche' esiste una combinazione di esecuzioni che causa errore.
Come abbiamo visto l'incremento di una variabile si divide in tre
istruzioni: evalutazione, somma, scrittura.
Guardate cosa potrebbe succedere, in ordine cronologico:
-Il thread secondario (chiamiamolo B) inizia a ciclare, incrementando tempo.
-Il thread primario (A) attende che l'utente preme il bottone.
-Quando viene premuto il bottone, il thread A sospende il thread B.
-Ipotizziamo che il thread B sia stato bloccato subito dopo aver eseguito la
parte dell'evalutazione, quindi si ferma dopo aver letto che su Tempo c'e'
il valore 1000.
-Il thread A sovrascrive tempo, ponendolo a zero.
-Il thread A fa ripartire il thread B.
-Il thread B riprende da dove si e' interrotto, ovvero dopo l'evalutazione.
-Il thread B esegue la somma, 1000+1, visto che 1000 l'aveva gia' letto.
-Il thread B scrive 1001 su Tempo.
-Ih thread B ripende a ciclare normalmente.
Risultato: l'azzeramento del timer non ha funzionato!
Prima di vedere come si corregge questa cosa, andiamo a vedere il altro
tipico problema di chi si avvicina ai Thread.
BUSY WAIT
Il busy wait e' se possibile un problema molto grave.
Sostanzialmente, si tratta di un'attesa, (ovvero un thread che aspetta che
succeda qualcosa) che consuma la CPU.
Ad esempio abbiamo un thread A che aspetta che finisca il thread B.
Il thread B e' cosi' fatto:
procedure TThreadB.Execute;
begin
Sleep(INFINITE); // non finira' mai :P
end;
Il thread A potrebbe fare questo:
procedure TThreadA.Execute;
begin
While Not b.terminated do
begin
end;
showmessage('finalmente e' terminato!');
end;
E in effetti questo funziona! Ma dove sta il problema? Provate
effettivamente ad eseguire questo programma. Se attivate il TaskManager e
controllate l'utilizzo della CPU, vedrete 100%, e sara' tutto consumato dal
vostro programmino!
Questo perche' il thread A non e' fermo! non e' in stato "sospeso in attesa
di un evento", ma e' pienamente funzionante. Il suo codice consiste
nell'evalutare sempre la condizione del ciclo while (che dara' sempre true)
e poi evalutarla ancora e ancora e ancora, senza fermarsi mai e usando
percio' tutte le risorse di CPU disponibili! E' un enorme spreco, visto che
in realta' il thread A non fa nulla di utile.
Questo problema e' molto grave perche' non da sintomi per l'utente
inesperto: il programma funziona! E per molti, questo basta e avanza, non
importa se funziona sprecando un sacco di risorse. Un singolo programma che
esegue una busy wait non da grandissimi problemi, ma pensate se tutti
facessero cosi'! Pensate se Notepad, in attesa che digitiate il testo, si
bevesse tutti i cicli di cpu disponibili.. Pensate se ICQ in attesa che
qualcuno vi mandi un messaggio se ne stesse li a occupare tutta la CPU..
Pensate ad Outlook in attesa di email, a un server FTP in attesa di
connessioni, a un database in attesa di query, al browser in attesa di
un'altra richiesta, a Campo Minato che attende la vostra prossima mossa ecc
ecc ecc... Semplicemente, sarebbe impossibile lavorare.
Quindi occorre stare bene attenti, perche' le busy wait si annidano ovunque
nei programmi multithread e spesso e' difficile vederle e capire che ci
sono.
Le stesse finestre di windows sono per la maggior parte del tempo in attesa
di qualche input dall'utente, e ovviamente eseguono una attesa pulita, non
busy. Il ciclo principale delle finestre e' questo:
while GetMessage(AMessage, 0, 0, 0) do begin
TranslateMessage(AMessage);
DispatchMessage(AMessage);
end;
La GetMessage si blocca finche' non arrivano nuovi messaggi, ma ovviamente
si blocca in modo pulito, senza consumare alcuna risorsa.
Ora vedremo come possiamo eseguire delle attese pulite e molte altre cose.
OGGETTI DI SINCRONIZZAZIONE
Gli oggetti di sincronizzazione sono gli strumenti che windows ci mette a
disposizione per risolvere i problemi delle attese e degli accessi
contemporanei a dati o risorse.
Questi oggetti sono di vari tipi, e prendono il nome di Eventi, Semafori,
Mutex, CriticalSection e Timer.
Prima di parlarne, una doverosa precisazione: per usare tutti questi
oggetti, normalmente si dovrebbero chiamare le apposite API di windows
(CreateEvent, CreateMutex, ecc ecc), ma Delphi ci aiuta mettendoci a
disposizione delle classi che incapsulano queste chiamate. Tratteremo
principalmente le classi di delphi spiegando dove serve come funzionano. Le
classi sono quasi tutte contenute nella unit SyncObjs.pas
TCriticalSection
La prima di queste e' TCriticalSection. E' molto utile e molto usata.
Funziona cosi': la classe rappresenta una "parte di codice" che va eseguita
senza interruzioni da parte di altri thread critici.
La classe fornisce due metodi: Acquire e Release (o gli identici Enter e
Leave).
Pensate al TCriticalSection come una bandierina. Solo un thread alla volta
puo' avere la bandierina, se qualcun'altro la vuole, deve aspettare che
venga rilasciata.
Praticamente funziona cosi': si crea un'oggetto TCriticalSection in modo che
abbia visibilita' globale (cioe' tutti i thread devono vederlo), e poi si
"chiude" il codice "rischioso" tra una chiamata Enter e Leave
Tornando all'esempio del "cronometro rozzo" di prima, potremmo fare cosi':
type TMyThread=class (TThread)
Tempo:integer;
procedure Execute; override;
end;
Var Bandierina:TCriticalSection;
Implementation
procedure TMyThread.Execute;
begin
while true do
begin
Bandierina.Enter;
inc(tempo);
Bandierina.Leave;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Bandierina.Enter;
Thread.Tempo:=0;
Bandierina.Leave;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Bandierina:=TCriticalSection.Create;
end;
Come vedete, il nostro oggetto (che ho chiamato Bandierina tanto per rendere
l'idea dell'"acquisire/rilasciare", ma voi chiamatelo con un nome piu'
appropriato) e' usato in modo tale da controllare l'accesso alla variabile
Tempo. Praticamente sia il Thread secondario che il thread della VCL devono
ottenere la bandierina se vogliono scrivere sulla variabile Tempo.
In questo modo anche se noi premiamo il pulsante Azzera mentre il thread sta
manipolando Tempo, l'azzeramento non sara' effettuato fintanto che il thread
non avra' finito le sue manipolazioni e non avra' rilasciato la bandierina.
La chiamata Bandierina.Enter si blocchera' finche' il thread nella sezione
critica non chiama Bandierina.Leave.
Possiamo osservare che non vi e' modo di sapere se un oggetto e' attualmente
aquisito o no. Possiamo solo chiamare Enter, e aspettare se c'e' da
aspettare :)
Bene, detto cio', abbiamo imparato come funzionano i TCriticalSection.
Solitamente vengono usati per proteggere una risorsa condivisa tra piu'
threads. Si usa un oggetto per ogni risorsa da proteggere.
Ovviamente bisogna poi chiamare le Enter e Leave ogni volta che la si usa,
perche' se create l'oggetto e poi quando accedete alla risorsa vi
dimenticate di acquisire, tutto il lavoro fa a farsi friggere!
E' una buona norma, se avete a che fare con risorse condivise, includere il
critical section direttamente nella risorsa. Ad esempio c'e' una classe,
TThreadList, fornita con Delphi, che e' come TList ma e' tutta protetta, e
quindi Thread-Safe. Non sempre si puo' fare cosi', ma se potete, vi
eviterete la rottura di chiamare in continuazione Enter e Leave.
TMultiReadExclusiveWriteSynchronizer
Questa classe, che vince la palma di "classe dal nome piu' lungo del mondo",
e' una variante di TCriticalSection. Stranamente, non si trova nella unit
SyncObjs, ma nella beneamata SysUtils. Chissa' perche'?
TCriticalSection, come abbiamo visto, permette a un solo thread alla volta
di entrare nella sezione critica. Ma questo, in alcuni casi, e' superfluo:
infatti non c'e' nessun problema se due thread accedono alla risorsa
protetta, purche' entrambi la leggano senza scriverla.
In sostanza, l'idea e' che qualsiasi thread puoi accedere in lettura, ma
solo un thread alla volta puo' accedere in scrittura.
TMultiReadExclusiveWriteSynchronizer fa proprio questo. Ha quattro metodi:
BeginRead, BeginWrite, EndRead, EndWrite
La regola e' questa: Se un thread vuole entrare in lettura, deve aspettare
che l'eventuale thread entrato in scrittura esca, mentre non deve attendere
che altri finiscano di leggere (ci accede in contemporanea). Se un thread
vuole entrare in scrittura, deve aspettare che TUTTI i thread (lettori e
scrittori) siano usciti.
Quando uno scrittore rilascia l'oggetto, vengono attivati o tutti i lettori,
o un'altro scrittore, a seconda di chi arriva per primo.
Questo oggetto e' molto utile quando si ha una risorsa che viene letta
spesso ma scritta molto raramente. Tanto per fare un esempio, un server Web,
che legge spesso le pagine che gli vengono richieste ma raramente le scrive
(quando il webmaster aggiorna il sito). In quel caso sarebbe utile usare il
TMultiReadExclusiveWriteSynchronizer invece del TCriticalSection.
Ad ogni modo, state attenti quando usate
TMultiReadExclusiveWriteSynchronizer: non sempre e' palese se un thread
accede in scrittura o in lettura. Assicuratevi che eventuali procedure che
chiamate tra BeginRead e EndRead non vadano a scrivere da qualche parte la
risorsa protetta.
TEvent
Il nostro prossimo amico nel mondo degli oggetti di sincronizzazione e'
TEvent. Questo si differenzia un po' dai due precedenti, e ha altri scopi.
Un TEvent rappresenta un evento, e puo' essere Attivo o Non attivo
(segnalato/resettato). I thread possono fare due cose: attivare o
disattivare un evento (cosa che di solito fa il thread di controllo) oppure
attendere che l'evento sia segnalato.
Quando un thread si mette in attesa di un evento con TEvent.WaitFor(timeout)
succede una delle seguenti cose:
-Se l'evento e' gia' segnalato, la chiamata ritorna subito e il thread
prosegue. La WaitFor ritorna wrSignaled.
-Se l'evento non e' segnalato, la chiamata si blocca, e ritorna in uno di
questi casi:
- Se passa il tempo specificato su Timeout (nel qual caso ritorna
wrTimeOut)
- Se l'oggetto TEvent viene distrutto, nel qual caso ritorna wrAbandoned
- Se capita qualche errore, nel qual caso ritorna wrError
- Se ovviamente l'oggetto diventa segnalato, ritornando wrSignaled
Specifichiamo che quando un evento diventa segnalato, TUTTI i thread in
attesa si sbloccano.
Questo oggetto e' molto utile quando si deve attendere che succeda qualcosa.
Un esempio puo' essere un thread che compie N operazioni, e alla fine di
ogni operazione un'altro thread esegue del codice (ad esempio mostra i
risultati intermedi, o pulisce la memoria, ecc ecc).
In generale e' utile quando c'e' qualcosa da aspettare.
In un mio programma ad esempio c'era un thread che aveva una lista di
operazioni che eseguiva una dietro l'altra finche' non le finiva. Altri
thread potevano aggiungere operazioni alla lista. Questi altri thread si
bloccavano subito dopo aver messo l'operazione nella lista, e il thread
delle operazioni li svegliava ogni volta che eseguiva quanto loro avevano
richiesto. Tanto per fare un esempio..
Oltre al TEvent esiste anche il TSimpleEvent, che e' una versione
semplificata del gia' semplice TEvent. Cambia solo il costruttore, dove
TSimpleEvent non prende alcun parametro, arrangiandosi a usare parametri che
vanno bene nella maggior parte dei casi.
Il TEvent si usa come gli altri oggetti: ne va creato uno per ogni "evento"
da attendere, che sia visibile a tutti i thread interessati. Lo si crea col
costruttore e poi si possono richiamare i metodi SetEvent e ResetEvent. Come
gia' detto il metodo WaitFor(timeout) attende che un evento diventi
segnalato, permettendo di specificare un timeout (INFINITE per infinito).
Ritorna un valore di tipo TWaitResult.
Che altro dire su TEvent? Boh, mi pare di aver detto tutto.
Semafori e Mutex
I semafori sono l'oggetto di sincronizzazione per antonomasia, ma
inspiegabilmente non hanno una classe corrispondente in Delphi..
Ad ogni modo, vi spiego cosa sono perche' tornano sempre utili. Praticamente
sono come i CriticalSection, solo che permettono l'accesso N alla volta
invece che 1 alla volta. Tutto qua :)
Quando li create (con le API di windows, visto che non c'e' la classe)
potete specificare il valore di N, che verra' memorizzato nell'oggetto.
Quando qualcuno aquisisce, N viene decrementato. Quando qualcuno rilascia, N
viene aumentato. Se N e' zero, un tentativo di acquisirlo blocchera' il
thread chiamante finche' qualcuno non rilascera' il proprio "permesso". Se
il CriticalSection e' una bandierina, il semaforo e' un mazzetto di
bandierine.
Per la cronaca, i mutex funzionano come le CriticalSection, solo che queste
ultime funzionano solo nel contesto dei thread si un singolo processo,
mentre i mutex fuzionano anche tra diversi processi.
COME SONO IMPLEMENTATE LE CLASSI DI DELPHI?
Le classi che abbiamo visto qui sopra sono tutte wrapper alle API del
sistema operativo. Questa e' la filosofia su cui e' basata l'intera VCL e se
vogliamo l'intero Delphi. Come per la VCL in generale, anche nell'ambito
degli oggetti di sincronizzazione possiamo trarre grandi benefici da questa
strutturazione.
Teoricamente, in Windows gli oggetti di sincronizzazione sono degli Handle,
che in generale sono degli interi che indicano alcuni dati presenti in
windows. Ad esempio un file aperto ha un handle, ogni processo ha un handle,
ogni thread ha un handle, ogni oggetto di sincronizzazione ha un handle. Le
api di Windows che si chiamano CreateXXXX (dove XXX e' file, event, mutex,
process, ecc ecc) ritornano un handle, mentre tutte le altre API per
maneggiare questi oggetti prendono ovviamente l'handle come parametro
identificatore. Ad esempio l'API SetEvent e' dichiarata cosi':
BOOL SetEvent(
HANDLE hEvent // handle of event object
);
Guarda caso, il metodo SetEvent di TEvent chiama proprio l'API SetEvent:
procedure TEvent.SetEvent;
begin
Windows.SetEvent(Handle);
end;
Niente di stupefacende: i metodi sono wrapper alle chiamate di windows.
Da questo si deduce che la classe TEvent incapsula anche un Handle.. e
infatti e' proprio cosi':
TEvent = class(THandleObject)
public
constructor Create(EventAttributes: PSecurityAttributes; ManualReset,
InitialState: Boolean; const Name: string);
function WaitFor(Timeout: LongWord): TWaitResult;
procedure SetEvent;
procedure ResetEvent;
end;
Come vedete deriva da THandleObject, che e' cosi' definito:
THandleObject = class(TSynchroObject)
private
FHandle: THandle;
FLastError: Integer;
public
destructor Destroy; override;
property LastError: Integer read FLastError;
property Handle: THandle read FHandle;
end;
Come vedete non fa altro che tenere in memoria un Handle e un LastError da
cui trarre informazioni in caso di errore.
Quindi sostanzialmente, possiamo "scavare" all'interno della gerarchia e
ottenere l'handle di un oggetto TEvent, e usare questo Handle con le
chiamate API, scavalcando i suoi metodi.
Chi di voi usa delphi intensivamente, sapra' che si puo' fare lo stesso in
molti altri campi. Tanto per la cronaca, si puo' ricavare l'handle di un
TCanvas, di un TForm, di un TButton, di un TBitmap, e di mille altre cose.
Ma perche' sporcarsi le mani con le chiamate di basso livello quando le
nostre classi di Delphi fanno quasi tutto da sole? Per il fatto appunto che
fanno "quasi" tutto! Alcune operazioni non sono sempre incluse nelle classi,
vuoi per semplicita', vuoi perche' solo marginalmente utili, vuoi perche'
solo presenti in WinNT, vuoi per qualsiasi altro motivo.
Tanto per fare subito un esempio, prendiamo l'oggetto TEvent. Esso come
abbiamo visto ha i metodi SetEvent e ResetEvent, che chiamano le
corrispondenti API di windows. Pero' c'e' una terza procedura che non ha il
corrispondente metodo, e che si chiama PulseEvent. Stupiti eh ?
La PulseEvent praticamente va chiamata quando l'evento e' non segnalato, ed
essa non fa altro che segnalare l'evento, sbloccando tutti i thread in
attesa, e subito dopo resettarlo a "non segnalato". O, detto in altri
termini, sblocca tutti i thread in attesa lasciando l'evento a "non
segnalato". Puo' tornare utile nel caso in cui l'evento deve essere
"istantaneo" e subito ribloccarsi.
Come si puo' intuire puo' essere utile in casi molto specifici, e in
generale non se ne ha bisogno.
Ma noi che vogliamo imparare, possiamo come avrete gia' capito, usare la
chiamata di sistema PulseEvent pur usando le nostre amate classi TEvent.
Cio' si fa semplicemente usando l'handle:
var Evento:TEvent;
...
PulseEvent(Evento.Handle);
...
Capito la finezza?
Bene, ora andiamo un puo' piu' avanti, e vediamo le API di windows chiamate
Wait Functions. Esse possono essere Singole o multiple, sono le seguenti:
SignalObjectAndWait, WaitForSingleObject, WaitForSingleObjectEx,
WaitForMultipleObjects, WaitForMultipleObjectsEx, MsgWaitForMultipleObjects
e MsgWaitForMultipleObjectsEx
Ne vediamo solo alcune di utili:
La prima, SignalObjectAndWait, e' molto utile. Permette di compiere
un'operazione "atomica" nella quale si segnala un oggetto e ci si mette in
attesa di un'altro. Il bello e' proprio il suo essere atomica, cioe'
indivisibile: si ha quindi la garanzia che le due sub-azioni (segnalare un
oggetto e attendere un'altro) siano eseguite senza che vengano interrotte da
altri thread. Se voi infatti fate semplicemente:
EventoA.SetEvent;
EventoB.WaitFor(INIFINITE);
potrebbe succedere che tra le due chiamate si intromettano altri thread che
potrebbero compiere operazioni sgradite come ad esempio il modificare uno
dei due eventi o chissa' cos'altro.
Ancora una volta, la SignalObjectAndWait puo' essere usata con gli Handle di
TEvent.
Questa e' la sua definizione:
BOOL SignalObjectAndWait(
HANDLE hObjectToSignal, // handle of object to signal
HANDLE hObjectToWaitOn, // handle of object to wait for
DWORD dwMilliseconds, // time-out interval in milliseconds
BOOL bAlertable // alertable flag
);
Come vedete prende due handle, un valore di timeout (come la WaitFor di
TEvent) e un flag aggiuntivo (che specifica se la chiamata si interrompe in
seguito ad alcuni eventi particolari, leggete l'help per maggiori
informazioni).
Quindi cio' che e' scritto prima si puo' tradurre come:
SignalEventAndWait(EventoA.Handle, EventoB.Handle, INFINITE, true);
Bello no ? :) Notiamo che questa funzione esiste solo su WinNT.
La WaitForSingleObject invece semplicemente si mette in attesa su un
determinato handle. E' cosi' definita:
DWORD WaitForSingleObject(
HANDLE hHandle, // handle of object to wait for
DWORD dwMilliseconds // time-out interval in milliseconds
);
Sembrerebbe di scarsa utilita', visto che le nostre classi hanno gia' i
metodi per mettersi in attesa.
Pero' dovete sapere che anche handle di altre cose possono essere usati per
"aspettare". In particolare anche l'handle dei Thread e dei Process vanno
bene per questa funzione. Cosa succede se si passa l'handle di un thread o
di un processo? Semplicemente attende finche' il thread/processo non
termina.
Guarda caso, il metodo WaitFor della classe TThread chiama tra le altre cose
proprio la WaitForSingleObject passando l'handle del thread.
La lista completa degli handle "waitable" e' la seguente:
ChangeNotification, ConsoleInput, Event, Semaphore, Mutex, Process, Thread e
Timer.
La WaitForMultipleObjects fa piu' o meno la stessa cosa, solo che attende un
certo numero N di oggetti.
DWORD WaitForMultipleObjects(
DWORD nCount, // number of handles in the object handle array
CONST HANDLE *lpHandles, // pointer to the object-handle array
BOOL bWaitAll, // wait flag
DWORD dwMilliseconds // time-out interval in milliseconds
);
La struttura si capisce da sola: semplicemente attende tutti gli handle che
gli si passa.
Se il Wait flag e' TRUE, la funzione attende che tutti gli oggetti siano
segnalati, se e' FALSE attende solo che uno qualsiasi di essi sia segnalato.
Bene, anche con questo argomento direi che siamo a posto.
Vediamo ora un esempio in cui l'uso dei Thread e' sempre il benvenuto: i
server multhreaded.
I SERVER MULTITHREADED
Un campo i cui i thread si usano intensivamente e' il campo dei server. Se
volete costruire un server come si deve, dovrete per forza usare i thread.
Le funzioni riguardanti le socket, sono quasi sempre bloccanti, ovvero non
ritornano finche' non succede quello che viene richiesto. Questo dovrebbe
farvi ripensare a tutto cio' che si e' detto prima...
I server in generale funzionano cosi':
Un thread principale crea la struttura del socket server, mendiante le
chiamate alle API. Di solito, e' qualcosa del genere:
var
address:sockaddr_in;
WSAdata:TWSAData;
test:TPartita;
serverSocket:TSocket;
begin
WSAStartup ($0101,WSAdata); //inizializziamo la DLL delle socket
FillChar(address, sizeof(address),0); // settiamo address dicendo che tipo
di socket vogliamo
address.sin_family := PF_INET;
address.sin_port := htons(PORTA);
serverSocket := socket(AF_INET, SOCK_STREAM, 0); // otteniamo il server
socket
bind(serverSocket,address,sizeof(sockaddr_in)); // lo bindiamo
listen(serverSocket, 10); // e iniziamo l'ascolto
...
...
end;
A questo punto, solitamente si entra in un ciclo in cui questo thread
principale inizia ad attendere l'arrivo di client. Cio' che fa e' questo:
-Attendi l'arrivo di un client (che si connette)
-Crea un thread che gestisce il client appena connesso
-Ritorna ad attendere un altro client
Ecco il classico corpo del metodo Execute di un thread che fa questo:
Procedure TServerThread.Execute;
var siz:integer;
s:Tsocket;
sockadd:Tsockaddr; // la stuttura contentente le info dell'ultima
connessione
begin
// questa e' la routine che gestisce il server
// continua a ciclare finche' terminated e' falso
while not Terminated do
begin
siz:=sizeof(sockadd);
s:=accept(ServerSocket,@sockadd, @siz); // accetta una nuova connessione
(la mette in s)
if not Terminated then
begin
// creiamo il thread...
CreaNuovoUserThread(s);
end;
end;
end;
Come vedete la funziona accept e' il cuore dell'operazione: essa attende
finche' non arriva un client, bloccando (in modo pulito ovviamente, senza
busy wait) il proprio thread. Quando qualcuno si connette, accept ritorna il
socket del client, con il quale si puo' costruire un nuovo thread.
Sostanzialmente questo e' il sistema secondo cui funzionano i server
multithreaded: un thread sta in attesa, e altri N thread dialogano con gli N
client connessi. In un server FPT o HTTP, ciascuno di questi "Thread utente"
lavorera' ignorando pressoche' il lavoro degli altri thread, ciascuno
operera' per conto suo. Nei server IRC, o nei mud, spesso i comandi dati da
un client generano effetti che vanno notificati ad altri client (ad esempio
se un tizio in IRC dice qualcosa, gli altri client ne vengono informati
cosicche' possano stampare la frase ad uso dei propri utenti).
Nei mud che sono un misto tra una chat e un gioco di ruolo (mi perdonino i
puristi per questa definizione) e' ancora piu' complicato, perche' gli
utenti possono interagire con entita' comuni. Ad esempio gli alter ego di
due giocatori potrebbero trovarsi in una stanza nella quale vi e' una spada.
Se entrambi danno il comando "prendi spada", e' necessario sincronizzare i
due accessi all'oggetto "spada" mediante le tecniche di sincronizzazione che
abbiamo visto poco fa.
Allo stesso modo un server SQL dovra' stare attento che le query dei suoi
client non si accavallino (ad esempio modificando lo stesso record) e per
far cio' dovra sincronizzarli.
Ma cosa fara' il thread utente in tutti questi casi? Seguira' piu' o meno
questo algoritmo:
while Connected do
begin
LeggiComando
EseguiComando
end;
Tutto qua. Se e' un server FTP, il comando potrebbe essere la gestione di
file e cartelle, se e' un HTTP potrebbe gestire le richieste di pagine, se
e' una chat potra' leggere e processare le frasi che inserisce l'utente, ecc
ecc ecc.
La struttura e' simile per tutti i tipi di server.
Vi domanderete: ma non sara' dispendioso avere un thread per ogni utente? E'
una questione comune, ma la risposta e' no, per vari motivi. Iniziamo col
dire che un thread "sospeso" non e' altro che una piccola tabella nella
memoria del sistema operativo, che contiene informazioni quali l'handle, la
posizione in memoria, lo stato ecc ecc.
I thread dei server sono sospesi per la maggior parte del tempo. Ad esempio
un server IRC ci impiega pochissimo tempo a processare un comando (in fondo
si tratta solo di processare un po' di stringhe e inviarle ai vari client),
e anche un chattatore forsennato non scrivera' piu' di una frase al secondo
dal suo teminale. In secondo un computer e' in grado di eseguire il parsing
di migliaia di stringhe.. Stesso discorso anche per i server FTP, se
vogliamo.
Inoltre tipicamente in questi casi le operazioni dei diversi client sono del
tutto indipendenti le une dalle altre, quindi suddividerle in thread
separati puo' essere anche piu' conveniente che cercare di eseguirle
contemporaneamente su un singolo thread.
In definitiva un thread di per se stesso, utilizzato nel modo corretto
(ovvero senza busy wait), genera un overhead del tutto tollerabile.
Alcuni sistemi operativi, come ad esempio BeOS, sono massiciamente
multithreading, nel senso che se volete fare un'applicazione che non sia
"HelloWord" dovrete per forza utilizzare diversi thread.
Ma sto divagando.. tornando ai nostri server, vi diro' che la chiamata
fondamentale nella ricezione dei dati e' l'api recv:
function recv(s: TSocket; var Buf; len, flags: Integer): Integer; stdcall;
Sebbene questa funzione possa essere chiamata anche in modo che non blocchi
mai, il suo uso tipico e' quello di bloccarsi finche' non giungono dei dati
dalla connessione. E' questo il punto in cui i thread dei client si bloccano
in attesa.
Bene. Questo e' quanto c'e' da sapere per poter usare i thread per bene.
Spero di aver fatto un po' di luce su queste entita', che possa penetrare
l'alone di diffidenza che i thread hanno sempre avuto, specie dai newbie.
Saluti Nicola {MSX} Lugato
nic...@lugato.net
> Lo posto qui in modo che i piu' esperti possano criticarlo e segnalarmi
> eventuali errori, e che i nuovi possano imparare a prendere confidenza con
> questi Thread troppo spesso ritenuti troppo complessi per essere usati.
>
> Fatemi sapere che ne pensate :)
Che ti sono grato :))
Hai scritto veramente un manuale, complimenti.
Me lo stampo e sara' lettura di stasera prima di addormentarmi :)
Io sono uno di quelli che, nonostante programmi da 20 anni, quando sente
la parola thread pensa "oddio ... no, stavolta meglio dio " :)
Grazie ancora !
Ciao.Alberto.
--
Alberto Rubinelli - A2 SISTEMI posted with Gravity 2.60b
Via Costantino Perazzi 22 - 28100 NOVARA (NO) - ITALY ICQ 49872318
E-Mail : alb...@retrocomputing.net Tel. 0321 640149
Web : www.retrocomputing.net Fax 0321 391669 BBS 0321 392320
> Io sono uno di quelli che, nonostante programmi da 20 anni, quando sente
> la parola thread pensa "oddio ... no, stavolta meglio dio " :)
8-|
L' unica cosa "difficile" dei threads è la condivisione delle variabili; la
concorrenza infatti è un argomento che è stato studiato e ristudiato!
Oggi esistono degli strumenti al servizio dei programmatori (mutex, semafori
etc.) che semplificano di gran lunga la vita! :-)
--
- Carlo -
non e' mica un bel complimento dirgli che i suoi tutorial fanno
addormentare! dopo tanta fatica!! che ingrato! vergogna!!!
Ciao, Sandro
;D
>non e' mica un bel complimento dirgli che i suoi tutorial fanno
>addormentare! dopo tanta fatica!! che ingrato! vergogna!!!
GOOD!!!!!
--
Morde.
morde[at]programmer[.]net
> Fatemi sapere che ne pensate :)
Veramente molto, molto interessante, grazie. Per curiositą, sul NG sono
gią passati altri tutorial di questo tipo che vado a recuperarli e
salvarli?
Lis
--
Posted via Mailgate.ORG Server - http://www.Mailgate.ORG
>Fatemi sapere che ne pensate :)
Che e' ben fatto, e che ti sono grato per questo contributo, per me e per il
ng :-)
> Fatemi sapere che ne pensate :)
Ancora devo leggerlo per intero, tuttavia mi sembra piuttosto ben fatto
(avrei alcune precisazioni, ma preferisco prima rileggerlo).
Se puoi crea un documento pdf o magari un documento compatibile word e
pubblicalo da qualche parte.
--
- Carlo -
Certo, poi fammi sapere.
> Se puoi crea un documento pdf o magari un documento compatibile word e
> pubblicalo da qualche parte.
Penso che creero' una versione html, che mettero' sul mio sito ed
eventualmente in altri posti..
Grazie a tutti per i complimenti, molto gentili :P
>> Ancora devo leggerlo per intero, tuttavia mi sembra piuttosto ben fatto
>> (avrei alcune precisazioni, ma preferisco prima rileggerlo).
>
>Penso che creero' una versione html, che mettero' sul mio sito ed
>eventualmente in altri posti..
>
>Grazie a tutti per i complimenti, molto gentili :P
Qua l'unico veramente gentile sei stato tu ad aver fatto questo lavoro
e averlo reso disponibile a tutti!
Se avro' occasione di usare i thread di sicuro avro' vita + facile :D
Ciao
--
. _ _______ ļ,ø*°`°*ø,ļ_ļ,ø*°`°*ø,ļ_ļ,ø*°`°*ø,ļ_ļ,ø*°`°*ø,ļ_
/| // /__ __/ Kirys <My e-mail is a working antispam e-mail>
/ |/ \ / / Kirys Tech 2000: http://www.kt2k.com
/__|\__\ /_/ 2000 ļ,ø*°`°*ø,ļ_ļ,ø*°`°*ø,ļ_ļ,ø*°`°*ø,ļ_ļ,ø*°`°*ø,ļ_