Vorrei parlare di una funzionalità “vecchia” quasi quanto il concetto di programmazione, ma molto spesso lasciata un po’ in disparte e denigrata.
A me piace definire un Thread come la più piccola unità autonoma di elaborazione, mi pare rende bene l’idea.
Intanto andiamo a vedere un Thread che cos’è: il Thread come concetto è una parte di programma che lavora in autonomia, cioè “gira” senza l’ausilio del programma stesso.
I Thread sono previsti da tutti i sistemi operativi e anche se non lo sapete, lavorano in “sottofondo” nel vs. programma … sono i Thread che di fatto fanno funzionare correttamente una buona parte dei programmi.
Chi programma normalmente, avrà sentito parlare sicuramente del “MAIN THREAD” o “Thread Principale”: questo è il cuore dei programmi, dove vengono gestiti ed elaborati tantissimi, anzi più di tantissimi, dati, metodi, eventi, I/O, etc ….
Il “MAIN THREAD” è quello che si occupa di inviare ad esempio “i tasti premuti” ai controlli grafici, di gestire la messagistica (sotto Windows ad esempio) di sistema, di elaborare senza possibilità di conflitto le parti grafiche del ns. programma, e tanto altro ancora.
Tralasciando la funzionalità del “MAIN THREAD”, che diciamo è di sistema e l’interazione con esso è limitata, vediamo invece cosa possiamo fare con un Thread.
Gli esempi che porto e la descrizione riguardano particolarmente il sistema operativo Windows, perché è su quello che normalmente lavoro, ma di fatto si può operare alla stessa maniera anche su tutti gli altri sistemi operativi, facendo gli opportuni distinguo.
Come accennavo un Thread è la più piccola unità autonoma di elaborazione:
1) Piccola come concetto inteso a definire che dal punto di vista logico “sotto” il Thread non esiste nulla, se non metodi, procedure che comunque non sono autonomi.
2) Autonoma perché il Thread “gira” in autonomia e a “farlo girare” è il sistema operativo. Le possibilità che ha il programma di gestione di un Thread sono:
a) Creazione e distruzione;
b) Sospensione;
c) Continuazione dalla sospensione;
La sospensione e la “continuazione” sono di fatto delle operazioni che, anche se lecite, sono ASSOLUTAMENTE SCONSIGLIATE e di fatto non vengono più usate. Vengono usate delle tecniche di “buona programmazione” per “bloccare” e “riesumare” un Thread, tecniche che poi vedremo.
Come si definisce un Thread ? Viene definito come una qualsiasi altra classe e di fatto non è dissimile da una classe.
Quando un Thread “parte”, viene eseguito il metodo Execute e il Thread rimane in “vita” sino a quando non si esce dal metodo.
Siccome il Thread è abbinato ad un puntatore (variabile di istanza) è sempre opportuno coordinare l’uscita dal Thread e poi porre a NIL la variabile.
Tutto ciò che viene costruito a RUNTIME DEVE ESSERE “DISTRUTTO” A RUNTIME, cioè se nel Thread alloco memoria come una classe, un array dinamico o altro, devo al termine del Thread deallocare le risorse.
Il Thread può terminare in un solo modo, cioè quando si esce dal metodo Execute.
a) Nella modalità “classica”, nel metodo Execute non c’è il WHILE, per cui una volta che la procedura ha eseguito le operazioni programmate a codice, esce naturalmente. Questo è usato sopratutto con i Thread anonimi.
b) Usando la proprietà pubblica Terminate del Thread e poi, come nell’esempio, uscire dall’EXECUTE testando se la proprietà “Terminated” è VERO.
Quando uso il costruttore “nativo” del Thread, il parametro che mi viene richiesto è un booleano che definisce se il Thread “parte” con il costruttore o lo stesso rimane in stato di sospeso, consentendo ad esempio di attivarlo in un secondo momento.
Ovviamente, eseguendo l’OVERLOAD del costruttore, si è comunque obbligati a dichiarare all’interno dello stesso questo “flag”: per default il Thread viene creato in condizione di “sospeso”.
Nel distruttore, che viene chiamato quando pongo a FREE il Thread, e non quando il Thread termina, posso gestire le risorse interne che ho creato a RUNTIME (nell’esempio l’evento).
Come in ogni classe, le variabili delle classe sono “private” rispetto alle istanze (potrei istanziare il Thread più volte, ma l’esempio riportato non è adatto a ciò).
Esiste una dichiarazione speciale per definire delle variabili comuni a tutte le istanze, ma non globali rispetto al programma.
La definizione è “threadvar” ma anche questa definizione è obsoleta, sostituita da tecniche di buona programmazione.
unit Unit2;
{$mode ObjFPC}{$H+}
interface
uses
Classes, SysUtils, SyncObjs;
var
//Evento usato per “gestire” il Thread
EvElaborazione: TEvent;
type
TElaborazione = class(TThread)
private
//Queste definizioni private non sono obbligatorie,
//ma io le uso per avere un supporto migliore
//Questo lo uso per mantenere il componente di riferimento
fComponent: Tcomponent;
//Questo mantiene l’indice del Thread, utile per vari scopi
fID: cardinal;
//Variabile di appoggio per il conteggio dei cicli eseguiti
fContaCicli: cardinal;
//Variabile che consente di "capire" se un ciclo è stato eseguito
fAggiornato: WordBool;
//Funzione da property che consente la lettura di cicli e l'azzeramento
//di fAggiornato
function getContaCicli(): cardinal;
//Procedura di esempio per il Synchronize
procedure UPDATECAPTION;
protected
//Questa definizione è obbligatoria
procedure Execute; override;
public
//Queste definizioni non sono obbligatorie,
//ma io le uso per avere un supporto migliore
property Aggiornato: WordBool read fAggiornato;
property ContaCicli: cardinal read getContaCicli;
constructor Create(AComponent: TComponent; ID: cardinal); overload;
destructor Destroy; override;
end;
implementation
//In questa Unit è definita la Form1 (la classica Form derivata da TForm)
uses Unit1;
//NON CHIAMARE MAI DIRETTAMENTE QUESTA PROCEDURA !!!!!!!
procedure TElaborazione.UpdateCaption;
begin
Form1.Caption := 'Aggiornato dal thread';
end;
function TElaborazione.getContaCicli(): cardinal;
begin
//Qui sarebbe utile metterci un semaforo,
//onde evitare un DEADLOCK !!! O altre tecniche ...
//fAggiornato viene scritto sia qui che nella EXECUTE
if fAggiornato then
fAggiornato := false;
result := fContaCicli;
end;
procedure TElaborazione.Execute;
var risultato: TWaitResult;
begin
NameThreadForDebugging('Elaborazione_'+fID.ToString);
{ Place thread code here }
while not Terminated do
begin
try
//Attende un sincronismo e poi procede
risultato := wrTimeout;
while risultato <> wrSignaled do
begin
risultato := EvElaborazione.WaitFor(infinite);
//Se viene settata la proprietà Terminated allora esci
if Terminated then
break;
end;
//Se viene settata la proprietà Terminated allora esci
if Terminated then
break;
//Da qui posso fare qualsiasi cosa e procedere con il
//codice del Thread
inc(fContaCicli);
//Qui sarebbe utile metterci un semaforo, onde evitare un
//DEADLOCK !!! O altre tecniche …
//fAggiornato viene scritto sia qui che nella getContaCicli
if not fAggiornato then
fAggiornato := true;
except on e:exception do
begin
if Terminated then
break;
end;
end;
end;
end;
constructor TElaborazione.Create(AComponent: TComponent; ID: cardinal);
begin
//Il Thread parte immediatamente dopo la creazione
inherited Create(false);
fComponent := Acomponent;
fID := ID;
fContaCicli := 0;
fAggiornato := false;
Priority := tpNormal;
//Creo l’evento che mi consente di gestire il Thread
EvElaborazione := TEvent.Create(nil, false, false,
'EvElaborazione_'+fID.toString);
end;
destructor TElaborazione.Destroy;
begin
EvElaborazione.Free;
inherited;
end;
end.
Come facciamo ad usare il Thread ?
Intanto dobbiamo dichiarare una variabile (locale, globale, etc …) che useremo per identificare il Thread:
var Elaborazione: TElaborazione;
Poi dovremo creare l’istanza, tenendo conto che nel codice Create del Thread abbiamo usato ‘overload’ e quindi si ritiene che si debba usare quella !!! A questa situazione non ci sono purtroppo molte soluzioni e se per sbaglio nel codice del programma viene chiamato il costruttore classico, è probabile (anzi in questo esempio è certo) che il Thread vada in un LOOP INFINITO ingestibile ….. Però, almeno chiamando Terminate, il Thread terminerà correttamente.
Elaborazione := TElaborazione.Create(self, 1);
Adesso il ns. Thread girerà eseguendo un conteggio dei cicli di elaborazione.
Però … se vado a controllare la proprietà pubblica ContaCicli del Thread, oops è sempre a zero. Quindi il Thread non gira ?
Il Thread stà girando, solo che attende che l’evento EvElaborazione venga generato, solo in questo caso eseguirà il codice. Poi torna ad attendere una ulteriore attivazione dell’Evento.
Se dentro il click di un pulsante ci metto:
EvElaborazione.SetEvent;
Ad ogni pressione del tasto otterrò che ContaCicli del Thread si incrementa di uno.
Ma come girerà il Thread ? Mi inchioderà l’interfaccia grafica ? E cosa NON posso fare all’interno del THREAD ?
Come abbiamo detto il Thread viene gestito dal sistema operativo attraverso il proprio Scheduling. Il Thread stesso viene “interrotto” e ripreso dal sistema operativo e quindi non è possibile in alcun modo definire la “temporalità” della sequenza: ad esempio si può interrompere prima dell’istruzione di incremento e riprendere molto tempo dopo (secondi, millisecondi, microsecondi, nanosecondi). In questo senso, il Thread è ASINCRONO rispetto al codice del programma. Non è possibile a priori sapere se il Thread ha eseguito il codice, senza inserire qualche variabile che definisca il tutto.
E’ possibile gestire la priorità di esecuzione attraverso la proprietà “Priority”: variando questa proprietà è possibile definire quanto è il tempo di “servizio” del Thread, fino al CRITICALTIME, DA USARE SOLO ED ESCLUSIVAMENTE per Thread con operazioni molto brevi ma che hanno necessità di essere eseguite spesso e di non essere interrotte.
Interessante, è il valore tpIdle che consente di eseguire il Thread a bassisima priorità, evitando di usare risorse nei momenti di carico di lavoro, ad esempio per effettuare operazioni marginali.
Nei sistemi Unix-Like, la priorità ha una funzionalità leggermente diversa e DEVE essere abbinata alla POLICY (qui può intervenire qualcuno che ha nozioni dell’uso in questi sistemi operativi).
Ma come vengono distribuiti i Thread tra i vari CORE del processore ? A questo ci pensa il sistema operativo che schedula il Thread tra i vari CORE … in genere non è bene controllare tale funzionalità, perché il SO è normalmente ottimizzato per fare ciò (dispone i Thread in maniera bilanciata onde evitare sovracarichi).
Se però si dovesse per qualsiasi motivo sfruttare l’allocazione manuale del Thread in un specifico CORE, si può usare la funzione API SetThreadAffinityMask (specifico per Windows).
Attenzione che nell’uso dei Thread, a parte il codice che noi scriviamo, intervengono molti altri fattori: la cache del processore, l’interscambio di eventuali dati tra i Thread se su diversi CORE, la gestione della memoria.
Per questo la affinity mask dovrebbe essere lasciata in gestione al SO. Questo vale a maggior ragione su sistemi con l’ultima tecnologia INTEL ibrida (ALDER LAKE) e il Thread Director integrato (disponibile per Linux e Windows).
Ulteriormente, bisogna fare molta attenzione su sistemi multiprocessori fisici (come su molte soluzioni con processori INTEL XEON): ne Lazarus ne Delphi hanno il gestore di memoria che “funziona” per applicativi distribuiti su più processori fisici o peggio ancora su sistemi “NUMA”.
Cosa succede alla mia interfaccia grafica quando uso un Thread: tipicamente nulla, l’interfaccia grafica non ne risente. Mouse, tastiera, eventi, filano via come se i thread non ci fossero, ovviamente se facciamo le cose con criterio.
I Thread vengono eseguiti tramite schedulazione, diciamo ipoteticamente in parallelo entro il numero massimo di core disponibili, e lo schedulatore garantisce il servizio a tutti i Thread, mantenendo quindi fluido l’uso dell’interfaccia grafica.
Cosa non posso fare dentro un Thread:
a) NON POSSO SCRIVERE UNA VARIABILE O ACCEDERE AD UNA RISORSA IN SCRITTURA CHE SIANO GLOBALI, SENZA UNA ADEGUATA PROTEZIONE (CHIAMATA FENCE). Devo assolutamente usare semafori, control sections, o altre tecniche di protezione per evitare che la risorsa sia contemporaneamente scritta da due “punti”, verrebbe generata una condizione di DEADLOCK con crash del programma e sua chiusura. Posso non usare tali tecniche se sono certo che la risorsa viene SCRITTA SOLO ED ESCLUSIVAMENTE dal mio codice del Thread nel metodo EXECUTE o metodi chiamati SOLO ed ESCLUSIVAMENTE dal metodo EXECUTE.
b) NON POSSO ACCEDERE in scrittura ad alcuna risorsa “grafica”, quindi alle LCL di Lazarus o VCL (o equivalenti) di Delphi, ove per risorsa grafica intendo proprio il componente stesso indipendentemente che la proprietà a cui accedo sia grafica o meno.
Ad esempio non posso fare accesso all’interno del THREAD in questo modo:
Form1.Caption := fID.ToString;
Non si deve mai accedere ai controlli, con o senza semafori direttamente dai THREAD !!!!
Le componenti grafiche non sono in genere “Thread Safe” e quindi non è possibile accedervi. Non ci sono eccezioni a ciò.
L’unico posto da cui si può accedere ai componenti grafici è il THREAD PRINCIPALE, quindi in pratica negli eventi generati dai componenti stessi.
Gli Eventi dei componenti sono garantiti essere sicuri per l’accesso a tutte le componenti grafiche dell’applicazione.
Ad esempio nell’evento di pressione di un Pulsante posso fare ciò che voglio dell’interfaccia grafica, senza preoccuparmi di nulla: questo perché il THREAD PRINCIPALE mi garantisce che le operazioni siano serializzate.
In realtà rispetto a questo c’è anche una gabola: non si devono usare in alcun modo funzioni che alterino lo scheduling del THREAD PRINCIPALE, ad esempio Application.ProcessMessages … pena pericolose sovrapposizioni. Ovviamente a meno che non sappia cosa stò facendo.
Per particolari condizioni, è consentito l’accesso alle CANVAS previo uso del LOCK della stessa e dell’UNLOCK (in alcuni ambienti LOCK e UNLOCK sono automatici).
Bisogna fare attenzione che lo scorretto uso inchioderà il programma senza via di uscita (o meglio l’unica via sarà il task manager di Windows o il kill di Linux).
Ma come faccio a usare un Thread per segnalare un risultato dell’elaborazione ?
Bhè, diciamo che si possono usare tutte le tecniche e le fantasie più perverse, dalle variabile globali (tipi, record, classi, etc …) alle proprietà pubbliche del Thread stesso, ai messaggi (in Windows ad esempio) agli eventi, alle callback, ……
Posso anche usare il metodo Synchronize per eseguire una procedura “grafica” o non grafica, tale procedura verrà eseguita nel THREAD PRINCIPALE al riparo da qualsiasi problematica. Tale procedura, ancorchè sia sconsigliata perchè surclassata dalla funzione TThread.QUEUE, la ritengo comunque valida (e le poche volte che la uso mi è sempre stata utile e semplice da usare).
La procedura può essere una procedura del Thread, o una procedura anonima (solo x Delphi) e verrà eseguita secondo lo scheduling del THREAD PRINCIPALE:
//Procedura definita nel Thread
Synchronize(UpdateCaption); //per Delphi
Synchronize(@UpdateCaption); //per Lazarus
//Procedura anonima (vale solo per Delphi, Lazarus non supporta questa modalità)
Synchronize(
procedure
begin
Form1.Caption := 'Aggiornamento tramite metodo anonimo'
end
)
);
Ultimo accenno: come distruggiamo un Thread ?
Abbiamo detto che un Thread termina quando si esce dal metodo EXECUTE, ma ciò non significa che le risorse vengono “disallocate”. Il Thread esiste come istanza ma non sarà mai più operativo.
Per liberare le risorse ci sono due strade:
1) Settare la proprietà FreeOnTerminate del Thread a True. Questo fa si che il Thread libererà le risorse (chiamando automaticamente il destroy) quando uscirà dall’EXECUTE. Tale metodo è utile solo per i Thread anonimi, in quanto per i Thread con variabile di istanza, la stessa ovviamente non viene “azzerata” e punterà a qualcosa che non esiste più … sinceramente non ho mai usato ne penso userò FreeOnTerminate.
2) Usare l’approccio suggerito in generale: chiamare il Terminate e poi il Free. Nel nostro caso, siccome il Thread ha un WAIT di un evento con attesa INFINITA, dovremmo ovviare simulando un evento:
//Sequenza per terminare il Thread di esempio
//Mette a True la proprietà TERMINATED del Thread
Elaborazione.Terminate;
//Genera l’evento per fare uscire l’EXECUTE dal WaitFor infinito
EvElaborazione.SetEvent;
//Attende che il Thread TERMINI EFFETTIVAMENTE
Elaborazione.WaitFor;
//Distrugge l’istanza
Elaborazione.Free;
//Pone a Nil la variabile di istanza
Elaborazione := nil;
La potenza di elaborazione dei Thread è ancora indiscussa, nonostante diverse filosofie di pensiero tendano a spostarsi sui ThreadPool o su altre tecnologie.
Posso generare decine di Thread, ognuno con elaborazioni specifiche, e se ben costruiti i Thread fanno un lavoro stupendo in background (ma non per questo in secondo piano o più lentamente) senza alterare la mia interfaccia e senza sovracaricare il PC.
Una considerazione finale sui Thread: i thread non hanno una coda di messaggi, quindi non possono ricevere alcun messaggio ne dall’applicazione ne dal SO (WINDOWS), come ad esempio WM_CLOSE. Si può “aggiungere” al codice di un Thread un gestore per la coda stessa, ma non ha molto senso, potendo gestire comunque gli eventi.
Se avete suggerimenti o volete fare qualche domanda riguardo questo articolo, potete usufruire del forum italiano su Lazarus e Free Pascal: