Multi-threading in Delphi

Multi-threading in Delphi

Postby DarkByte » 26 Aug 2010, 13:27

Probabil ca ati auzit de multi ori de "thread-uri", de "multi-threading", "thread-ul principal" si alte notiuni asemanatoare. Acum e momentul sa clarificam ce inseamna lucrurile astea si sa vedem cum putem lucra cu thread-uri in Delphi.


Thread si multi-thread



Una din caracteristicile importante ale sistemelor Windows, incepand cu Windows 95, este posibilitatea de a lansa mai multe fire de executie dintr-un singur proces. Mult timp dupa ce au aparut primele calculatoare (si, evident, programarea), programarea a ramas "single threaded". Iti perforai cartelele, le duceai la centrul de calcul si, dupa cateva zile, primeai un alt teanc de cartele care contineau, daca aveai noroc, ceea ce vroiai. Totul se procesa in ordine, FIFO (primul venit, primul servit), nu era nicio graba, iar cand programul tau rula, calculatorul era "atent" doar la el.


computer cards, punched holes


Conceptul de "multiple threads" (mai multe fire de executie a aparut prima oara cu sistemele "time sharing", unde mai multe persoane puteau accesa acelasi computer si resursele lui, intr-un mod concurent (aparent, in acelasi timp). Calculatorul era responsabil pentru intretinerea acestei iluzii, impartindu-si timpul intre userii logati (de obicei, prin verificari secventiale pe fiecare statie conectata la el).
DOS si primele versiuni de Windows puteau rula un singur program la un moment dat. Cu timpul, pe masura ce aplicatiile au devenit tot mai sofisticate (games, anyone ? :D), conceptele de multi-tasking si multi-thread au devenit un lucru obisnuit (cate aplicatii ai deschise in momentul asta si cate thread-uri are fiecare ?).

Orice proces (aplicatie) contine cel putin un thread, denumit "thread-ul principal". Acest thread (fir de executie) este responsabil cu procesarile de mesaje (cele din Windows sau din aplicatii), modificarile vizuale ale programului, etc. In mod normal, nici nu am avea nevoie de alte threaduri, fiindca programul poate executa tot ce are de executat din threadul principal. Totusi, cu toata viteza calculatoarelor de ultima generatie, in momentul in care executam o bucla cu zeci de mii de cicluri (sau mai mult), ne putem trezi cu aplicatia "blocata" - nu mai raspunde la tastatura si mouse, nu-si mai face update-urile vizuale sau Windows ii va adauga in titlu faimosul "Not responding". Vi se pare familiara imaginea de mai jos ?


not responding, excel


Putem imbunatati situatia asta prin a spune programului sa proceseze mesajele primite, o data la cateva cicluri, dar asta va incetini operatia (calculele noastre), iar interfata oricum se va bloca daca procesarile nu sunt setate la intervale indeajuns de mici.

Solutia e simpla: lasi thread-ul principal sa-si faca procesarile in ritmul lui si lansezi un nou thread care sa faca "all the dirty work" in spate. Periodic, poti chiar "raporta" thread-ului principal de progresul operatiei pe care o executi. Vrei sa copiezi cativa GB de fisiere din programul tau ... sau sa calculezi PI cu 2000 de zecimale ? Probabil ca ar fi momentul sa afli ce sunt si cum se folosesc thread-urile.

Thread-uri in Delphi



Pentru a folosi un thread in aplicatie, ne trebuie un descendent de-al clasei TThread. Desi e destul de simpla crearea manuala a unei clase descendenta din TThread, putem folosi si pasii urmatori:

(File / New / Other / Thread Object)
delphi, new, other delphi, new items, thread object

Dupa introducerea numelui pentru noul thread, va va aparea un nou unit (in proiectul deschis) care va contine urmatoarele linii (clasa mea de thread a primit numele "TDarkThread", iar unitul a fost salvat cu numele "uDarkThread"):
  1. unit uDarkThread;
  2.  
  3. interface
  4.  
  5. uses
  6.   Classes;
  7.  
  8. type
  9.   TDarkThread = class(TThread)
  10.   private
  11.     { Private declarations }
  12.   protected
  13.     procedure Execute; override;
  14.   end;
  15.  
  16. implementation
  17.  
  18. { Important: Methods and properties of objects in visual components can only be
  19.   used in a method called using Synchronize, for example,
  20.  
  21.       Synchronize(UpdateCaption);
  22.  
  23.   and UpdateCaption could look like,
  24.  
  25.     procedure TDarkThread.UpdateCaption;
  26.     begin
  27.       Form1.Caption := 'Updated in a thread';
  28.     end; }
  29.  
  30. { TDarkThread }
  31.  
  32. procedure TDarkThread.Execute;
  33. begin
  34.   { Place thread code here }
  35. end;
  36.  
  37. end.

Codul care trebuie executat in thread va trebui pus in procedura Execute.

Initializarea thread-ului



Daca avem nevoie sa initializam variabile folosite in thread, sau vrem sa pornim thread-ul cu anumiti parametri, trebuie sa ne definim metoda (constructorul) Create. Tot in constructor ne putem seta prioritatea implicita a thread-ului si daca thread-ul va fi distrus automat in momentul in care isi termina executia. Constructorii (oricarei clase) trebuie declarati in sectiunea "public" sau "published". Codul va arata in felul urmator:

  1. unit uDarkThread;
  2. ...
  3.  
  4.   public
  5.     constructor Create(aLines: Integer; CreateSuspended: Boolean = True);
  6.   end;
  7.  
  8. ...
  9.  
  10. constructor TDarkThread.Create(aLines: Integer; CreateSuspended: Boolean = True);
  11. begin
  12.   inherited Create(CreateSuspended); // thread-ul va fi creat, dar nu va rula
  13.                                      // pentru a-l rula, folosim "Resume" (sau "Execute")
  14.   FreeOnTerminate := True;           // thread-ul va fi distrus automat cand termina
  15.   Priority := tpLower;               // prioritatea thread-ului este cu doua puncte sub prioritatea normala
  16.  
  17.   NumLines := aLines;                // initializam o variabila
  18. end;
  19.  
  20. ...
  21.  

La implementarea unei proceduri constructor, denumita de obicei "Create" (desi nu e limitata la acest nume), ar trebui sa fiti atent daca trebuie sau nu sa apelati constructor-ul parintelui, folosind linia:

Acest apel asigura faptul ca obiectul tocmai creat a fost instantiat corect din clasa parinte, chiar daca parintele este TObject (care nu face nimic in constructor), dar in anumite cazuri poate sa nu fie necesar. Dupa cum ati vazut in cod si in exemplu, apelul de inherited poate fi diferit, in functie de necesitati.

Prioritati pentru thread-uri
Valoare
Prioritate
tpIdle
Thread-ul se va executa doar cand sistemul nu este folosit (idle). Windows nu va intrerupe alte thread-uri pentru a executa acest thread.
tpLowest
Prioritatea thread-ului este cu doua puncte sub normal.
tpLower
Prioritatea thread-ului este cu un punct sub normal.
tpNormal
Thread-ul are prioritate normala.
tpHigher
Prioritatea thread-ului este cu un punct peste normal.
tpHighest
Prioritatea thread-ului este cu doua puncte peste normal.
tpTimeCritical
Thread-ul primeste cea mai mare prioritate.


Executia thread-ului



Cum spuneam mai sus, Execute este metoda in care punem codul care-l vrem executat in thread. Intoarcerea din apelul lui Execute opreste thread-ul, apeleaza procedura asignata evenimentului (event handler) OnTerminate (daca exista una asignata). Execute ar trebui sa verifice periodic proprietatea Terminated (a thread-ului), pentru ca aceasta semnaleaza daca se doreste oprirea thread-ului din exterior. Daca aceasta proprietate este setata pe True, trebuie sa iesim din Execute - sau thread-ul poate cauza probleme la apelarea metodei Terminate.

Sincronizarea thread-ului cu thread-ul principal



Cum afisam progresul unui thread (indiferent de ce forma e acel progres) ? Daca thread-ul principal este responsabil cu interfata programului, inseamna ca thread-urile secundare trebuie sa comunice cu acest thread principal pentru update-uri vizuale. Dar ce se va intampla daca mai multe thread-uri incearca sa acceseze aceeasi variabila / componenta simultan ? Raspunsul e simplu: se vor crea conflicte care pot duce la crash-uri. Access violations, erori ciudate, tot pachetul.

Rezolvarea e putin mai complicata: toate metodele care acceseaza thread-ul principal (in orice fel) trebuie sa fie sincronizate. Mai pe inteles, daca apelam (in mod sincronizat !) o metoda din thread-ul principal din mai multe thread-uri simultan, accesul la acea metoda se va face secvential, in ordinea apelurilor si a prioritatii thread-urilor - in acest fel se evita conflictele de orice natura.

Pentru a sincroniza aceste apeluri inter-thread-uri, Delphi pune la dispozitie, prin intermediul clasei TThread, o metoda denumita Synchronize (deci clasa noastra derivata din TThread contine aceasta metoda !), care accepta un singur parametru - un nume de procedura. In acest fel, codul care face update de progres, de exemplu, poate fi mutat intr-o procedura care face doar acest lucru, fiind apelata prin Synchronize, ca si in exemplul de mai jos:
  1.  Synchronize(MyUpdateProgress);

Procedura care updateaza interfata din thread se numeste "MyUpdateProgress" si nu va cauza niciun fel de probleme (daca este folosita sincronizat !).


Alte informatii despre thread-uri



Daca ati creat un thread suspendat (nu l-ati pornit), puteti accesa si modifica variabilele expuse in sectiunea "public". In acest fel, initializarile se pot face si fara a folosi noi parametri in constructor.

Un thread poate lansa in executie alte thread-uri. Sky is the limit (the available memory, to be more precise).

Program exemplu



Dupa ce-ati citit tot ce-am scris pe aici, cu siguranta ca ati vrea sa vedeti si un exemplu, asa ca m-am gandit sa fac un program-test care sa verifice daca un numar e prim, folosind un thread separat pentru fiecare numar introdus pentru verificare. Sa vedem putin cod.
  1. unit uDarkThread;
  2.  
  3. interface
  4.  
  5. uses
  6.   Classes, Sysutils;
  7.  
  8. type
  9.   TDarkThread = class(TThread)
  10.   private
  11.   public
  12.     NumberToCheck: int64;
  13.     IsPrime: Boolean;
  14.  
  15.     procedure UpdateResult; // metoda sincronizata pentru afisarea rezultatului
  16.     procedure Execute; override;
  17.  
  18.     constructor Create(aNumber: int64; CreateSuspended: Boolean = False);
  19.   end;
  20.  
  21. implementation
  22.  
  23. uses uMainForm;
  24.  
  25. constructor TDarkThread.Create(aNumber: int64; CreateSuspended: Boolean = False);
  26. begin
  27.   inherited Create(CreateSuspended); // thread-ul va fi creat, dar nu va rula
  28.                                      // pentru a-l rula, folosim "Resume" (sau "Execute")
  29.   FreeOnTerminate := True;           // thread-ul va fi distrus automat cand termina
  30.   Priority := tpNormal;              // prioritatea thread-ului este normala
  31.  
  32.   NumberToCheck := aNumber;          // initializam o variabila
  33. end;
  34.  
  35. procedure TDarkThread.Execute; // verificam daca numarul e prim
  36. var
  37.   i, factors: int64;
  38. begin
  39.   factors := 0;
  40.   try
  41.     IsPrime := True;
  42.  
  43.     // Vrem ca verificarea sa mearga cat mai incet,
  44.     // pentru a se putea observa functionarea thread-urilor.
  45.     // Din acest motiv, codul de mai jos nu este deloc optimizat.
  46.     i := 2;
  47.     while (NumberToCheck > i) do
  48.       begin
  49.         if (NumberToCheck mod i = 0) then
  50.           begin
  51.             Inc(factors);
  52.           end;
  53.  
  54.         if Terminated then
  55.           Exit;
  56.  
  57.         Inc(i);
  58.       end;
  59.   finally
  60.     IsPrime := (factors = 0);
  61.     Synchronize(UpdateResult);
  62.   end;
  63. end;
  64.  
  65. procedure TDarkThread.UpdateResult;
  66. begin
  67.   // anuntam daca numarul verificat este prim sau nu,
  68.   // daca thread-ul nu a fost terminat din exterior
  69.   if not Terminated
  70.     then
  71.       if IsPrime
  72.         then frmMain.MemoPrimes.Lines.Add(IntToStr(NumberToCheck) + ' is prime.')
  73.         else frmMain.MemoPrimes.Lines.Add(IntToStr(NumberToCheck) + ' is NOT prime.')
  74.     else frmMain.MemoPrimes.Lines.Add('Thread terminated (' + IntToStr(NumberToCheck) + ')');
  75. end;
  76.  
  77. end.

Dupa cum se si observa, codul care verifica daca numarul NumberToCheck este prim este pus in procedura Execute, asa cum am explicat si mai sus. Pe langa codul efectiv de verificare, in procedura Execute exista doua lucruri in plus. Primul ar fi:
  1.         if Terminated then
  2.           Exit;

Sa presupunem ca userul care incearca programul introduce un numar imens (sa zicem, cateva miliarde). Pana se poate determina daca acest numar e prim sau nu, pot trece chiar minute intregi (in implementarea curenta :P). Un buton care sa-i permita userului sa opreasca thread-ul ar fi binevenit, dar pentru ca un thread sa poata fi oprit fara probleme, el trebuie sa-si verifice periodic proprietatea Terminated. Aceasta proprietate este setata pe True in momentul in care se apeleaza procedura Terminate a thread-ului. E ca si cum ai calca frana masinii, dar dureaza ceva timp pana masina e intr-adevar oprita. E preferabil, totusi, ca intervalul din apelul procedurii Terminate si verificarea proprietatii Terminated sa fie cat mai mic, acesta fiind motivul pentru care verificarea proprietatii se face in fiecare ciclu al buclei de verificare a numarului.

A doua bucata de cod care atrage atentia este urmatoarea:
  1.     Synchronize(UpdateResult);

Asa cum am explicat mai sus, metodele care lucreaza cu date sau metode din alte thread-uri, trebuie sa fie sincronizate. Procedura mea, denumita UpdateResult, afiseaza intr-un TMemo daca numarul este prim sau nu, dar trebuie apelata in mod sincronizat, in eventualitatea ca un alt thread ar incerca sa faca acelasi lucru, in acelasi timp.

In forma actuala a acestui cod exista posibilitatea lansarii simultane a mai multor thread-uri (care sa verifice, fiecare, primalitatea unui numar) folosind o variabila locala din event handler-ul vreunui buton (din GUI). Cand un thread termina aceasta verificare si afiseaza rezultatul, el se va opri, iar memoria alocata lui va fi eliberata. Singura problema e ca, desi pot porni mai multe thread-uri, nu voi putea opri niciunul - va trebui sa astept pana se termina singure (sau sa opresc programul din Task Manager). This stinks, right ?

Ca sa ramanem cu posibilitatea de a lansa thread-uri multiple in executie, dar sa le putem opri cand dorim, va trebui sa ne creem un array de thread-uri. Fiindca nu vrem sa limitam potentialul de testare al programului, vom crea un array dinamic (ne va permite sa stocam oricate elemente, in limita memoriei sistemului). Codul urmator este ce ne trebuie:
  1.    cntThreads: Integer;
  2.    DarkThreads : array of TDarkThread;

Un contor de thread-uri (sa aflam repede si usor cate thread-uri stocam in orice moment dat) si array-ul in sine - acestea vor fi declarate ca variabile in thread-ul principal al programului - in clasa formei principale (in cazul de fata). Pentru a crea un thread nou, care sa verifice numarul dat, vom folosi ceva in genul urmator:
  1. procedure TfrmMain.btnCheckClick(Sender: TObject);
  2. begin
  3.   Inc(cntThreads);
  4.   SetLength(DarkThreads, cntThreads);
  5.   DarkThreads[cntThreads - 1] := TDarkThread.Create(StrToInt64(edtNumber.Text));
  6.   DarkThreads[cntThreads - 1].Resume;
  7. end;

Din codul de mai sus se observa ca forma mea principala contine un TEdit (editbox) cu numele "edtNumber" si un buton denumit "btnCheck". In momentul apasarii butonului, se creeaza un thread nou (in array) pentru a verifica numarul din casuta text. In acest fel, putem opri oricare thread - sau toate, folosind o bucla - din cele din array-ul DarkThreads. O metoda care opreste toate thread-urile:
  1. procedure TfrmMain.btnStopClick(Sender: TObject);
  2. var i: Integer;
  3. begin
  4.   if (cntThreads > 1)
  5.     then
  6.       for i := 0 to cntThreads - 1 do
  7.         DarkThreads[i].Terminate
  8.     else memoPrimes.Lines.Add('No threads running.');
  9. end;

Dupa cum se vede, inca un buton si se opresc toate thread-urile. Simplu ca "multi-threading". Daca nu avem niciun thread lansat, afisam lucrul acesta - in caz contrar, trimitem tuturor thread-urilor decizia de a le "concedia".

Damn threads. Tocmai a aparut o problema. Ce se va intampla in cazul in care lansam in executie 10-15 thread-uri - le lasam sa-si termine treaba - si apoi dam click pe butonul de oprire al thread-urilor ? :-s Thread-urile sunt dealocate, iar ce a ramas in array-ul ala dinamic doar naiba mai stie. Ar fi frumos (si chiar politicos) ca fiecare thread sa anunte cand se termina pentru a-l putea sterge din array. Procedura de stergere din array e destul de simpla - ne bazam pe handle-ul thread-ului si mutam elementele din dreapta thread-ului terminat cu o pozitie mai in stanga.
  1. procedure TfrmMain.DeleteThread(aHandle: THandle);
  2. var
  3.   i, j: Integer;
  4. begin
  5.   for i := 0 to cntThreads - 1 do // cautam thread-ul care trebuie sters din lista
  6.     if (DarkThreads[i].Handle = aHandle) then
  7.       Break;
  8.  
  9.   for j := i to cntThreads - 2 do // le mutam pe celelalte cu o pozitie in stanga
  10.     DarkThreads[i] := DarkThreads[i + 1];
  11.  
  12.   Dec(cntThreads);                    // actualizam contorul de thread-uri
  13.   SetLength(DarkThreads, cntThreads); // si lungimea array-ului
  14. end;

Cum spuneam, e simplu. Tot ce mai trebuie e ca, atunci cand afisam rezultatele din thread, sa apelam si aceasta procedura intr-un apel sincronizat pentru a curata array-ul. Daca apelul acestea proceduri nu este sincronizat, array-ul va ... - sa spunem doar ca programul nu va muri de moarte buna decat cu ajutorul unei doze generoase de noroc chior :P

Pentru a nu va complica inutil, aplicatia completa (sursa si executabil) sunt atasate - 185 de linii de cod, din care jumatate (poate) sunt cod scris manual.

Un screenshot cu programul:
multithread, prime numbers, GUI

Un screenshot cu Task Manager-ul in timpul rularii programului:
threads, Task Manager, prime

Daca nu aveti (in Task Manager) coloana care afiseaza numarul de thread-uri din procese, folositi urmatorii pasi:
thread column, Task Manager, settings

Note finale
  • Ar fi bine sa oprim toate thread-urile in momentul in care se incearca inchiderea programului (lucru implementat in aplicatia de mai jos).
  • Testati programul incercand un numar mare (pentru care programul sa piarda cel putin 30 de secunde) si, imediat dupa, un numar mic. Veti vedea ca, desi numerele au fost date intr-o anumita ordine, rezultatele pot aparea si in alta ordine (datorata folosirii mai multor thread-uri).
7p / 2 votes
Attachments
DarkThreads_exe.zip
(201.92 KiB) Downloaded 35 times
DarkThreads_src.zip
(92.2 KiB) Downloaded 44 times
User avatar
DarkByte
11011011
 
Joined: 29 Dec 2009
Status: 136

Return to Tutoriale Delphi

Who is online

Users browsing this forum: No registered users and 0 guests

cron