Elôzô cikkünkben a Thread-ek, avagy szálak elméleti részével ismerkedhettünk meg. Most a gyakorlati részt tekintjük át nagy vonalakban. Azért írom, hogy nagy vonalakban ugyanis számos - faragott, barkácsolt - példát hozhatnánk, de a legjobb ötleteket úgy is majd az élet hozza. Számos helyen alkalmazható, részint programunk gyorsítására a feladatok párhuzamosításával, vagy kevésbé fontos, illetve idôigényes feladat háttérbe küldésével. A lényeg a megvalósítás menete, ezért hagyatkozunk csak erre.

Barkácsolt példa 1

Próbáltam valami kicsit látványos példát kitalálni... Nem tudom, hogy sikerült-e, majd a tisztelt nagyérdemű eldönti. (Szerintem nem valami látványos, illetve nem egy konkrét mindennapi problémát fed le, viszont jól láthatóak rajta a szálaknál megoldandó problémák.) A lényeg, hogy van két szál, ami számolgat. A számolási eredményeiket állandóan kiírják, minden ciklus után.

Elôször is hozzunk létre egy form-ot, majd a rajta látható komponenseket:

Van rajta: két TGroupBox, benne TLabel-ek, egy TeditBox illetve egy TComboBox.

A felsô label (ThreadX, illetve ThreadY) írja majd ki a számolás értékét. Az editbox megadja, hogy az adott szál meddig számoljon el, továbbá beállítható az egyes szálak prioritása is a TComboBox segítségével.

Hozzunk létre egy új szál (thread) osztály a File menü / New ... / Thread Object ponttal, majd adjuk meg a nevét: TSzamoloThread.

Ennek hatására a Delphi létrehoz egy új unit-ot, illetve az új szál objektum struktúráját. Ebbôl a közös, közös típusú szálból származtatunk két hasonló feladatú szálunkat melyeket - fantázia dúsan - a továbbiakban ThreadX, ThreadY névvel illetünk. De azért ne szaladjunk a dolgok elébe, lássuk a közös ôs felépítését, metódusait:

  TSzamoloThread = class(TThread)   private     Ertek: Integer;     Probak, Szam: Double;     UseLabel: TLabel;     MaxCount: Integer;   protected     procedure Execute; override;     procedure szamol; virtual; abstract;     procedure kiir;   public     constructor Create(ALabel: TLabel; AMax: Integer);   end;

Az definiált változók, pontosabban objektum jellemzôk:

A thread belsô változói:

  • Ertek - a kiírandó ciklusváltozó értéke
  • Probak - a szám keresés próbálkozásának átlagos száma
  • Szam - a thread-ben összeadott véletlenszerű számok összege
A thread-nek átadott paraméterek:
  • UseLabel - a thread ebbe a fô form-on lévô label-be írja ki az aktuális eredményt
  • MaxCount - a ciklus maximális értéke

Lássuk az implementált metódusokat:

constructor TSzamoloThread.Create(ALabel: TLabel; AMax: Integer); begin   Inherited Create(False); {Hogy a Create() után rögtön elinduljon.}   UseLabel:=ALabel;   MaxCount:=AMax;   Szam:=0; end;

Elsô sorában meghívjuk az örökölt Create() metódust, hamis értékkel, hogy rögtön elindulhasson a szál futtatása. A paraméterként megadott változókat elmentjük az objektum megfelelô mezôibe.

procedure TSzamoloThread.Execute; begin   Szamol; end;

Az alapértelmezett Execute metódust átírjuk - itt kell lennie a végrehajtanó kódnak - és a Szamol metódust hivatjuk meg vele. A Szamol metódust ebben az ôs objektumban absztraktként definiáljuk, majd származtatott ôsében kerül az implementálásra sor.

procedure TSzamoloThread.Kiir; begin   UseLabel.Caption:='ciklus i: ' + IntToStr(Ertek) + ' Átlag próbák: ' + FloatToStr(Probak) + ' Szám: ' + FloatToStr(Szam); end;

A Kiir metódus az aktuális számolási értéket írja ki. Ezt közvetlenül nem hívhatjuk meg, ugyanis vizuális komponenseket csak a fô szálból lehet módosítani, így a már tárgyalt Synchronize() metódus segítségével hívhatjuk meg.

Lássuk az örökített szálak definícióját:

  TSzamoloThreadX = class(TSzamoloThread)   protected     procedure szamol; override;   end;   TSzamoloThreadY = class(TSzamoloThread)   protected     procedure szamol; override;   end;

Látható, hogy csak a Szamol metódust kell átírnunk, hisz ez végzi a szál fô tevékenységét.
Lássuk az egyes szálak Szamol metódusának implementációját:

procedure TSzamoloThreadX.Szamol; var   i, r: Integer;   LProbak: Longint; begin   Randomize;   for i:=1 to MaxCount do   begin     r:=Random(MaxCount-i);     repeat       Inc(LProbak);     until r = Random(MaxCount-i);     Probak:=(Probak*(i-1)+LProbak)/i;     Inc(Ertek);     Szam:=Szam+r;     Synchronize(Kiir);   end; end;

A szálban véletlen számokkal fogunk variálni. Egy ciklusban véletlen számot fogunk keresni a MaxCount-szor. A két szál minimálisan különbözik csak egymástól. Lássuk mit is csinál a ciklus:

  • Az r változóba egy véletlen számot generálunk, melynek maximális értéke MaxCount-i, tehát csökken a maximális érték a ciklus elôrehaladása során. Ez azért érdekes, mert másik szál pont fordítva, tehát az elôrehaladás során növeli a maximális értéket. Ez ott fog apró különbséget jelenteni a két szál között, hogy a repeat - until ciklusban keresni fogjuk az r változó értékét, s minél nagyobb lehet ez az érték annál többször kell véletlen számmal próbálkozni.
  • A repeat - until ciklusunk addig pörög, míg meg nem találja ugyanezt a véletlen számot, s közben jegyzi a próbálkozások számát.
  • A kiírandó próbálkozások számát adja meg a Proba változó.
  • Az Ertek változó a ciklus számláló jelenlegi értékét.
  • A Szam változó az eddigi véletlen számok összegét.
  • A kiíratás csak ekkor történik meg a Kiir metódussal, melyet a fô szálból kell meghívnunk, így a Synchronize() metódust használjuk. (A kiíratások, rajzolások miatt érdemes jól megválasztani a szálak prioritását, tehát nem túl nagyra tenni, mert akkor a fô szál nem tud majd semmit szépen kiírni.)
Lássuk a ThreadY szál implementációjakor felmerülô különbségeket:
    ...     r:=Random(i);     LProbak:=0;     repeat       Inc(LProbak);     until r = Random(i);

Látható, hogy egyedül a cikluson belül, a véletlen szám keresésénél különbözik. Ezt már fent meg is említettem. Erre azért van szükség, hogy valami azért különbözzön a két szál között. J

Nos most már kész is vannak az egyes szálak, viszont ezeket el is kell indítani: kezelni, paraméterezni, figyelni. Ezt a fô form-ban tesszük meg.

A fô form-ban, a ThreadForm-ban az alábbi mezôket és metódusokat kell definiálnunk:

  TThreadForm = class(TForm)     {...}   private     ThreadX, ThreadY: TSzamoloThread;     ThreadsRunning: Integer;     procedure ThreadDone(Sender: TObject);     function  GetPriority(ComboBoxIndex: Integer): TThreadPriority;   end;

A ThreadX és ThreadY szálakat futásidôben hozzuk létre. A ThreadDone() metódus egy figyelmeztetést ad, ha a szál befejezte munkáját. Erre a gyakorlatban is szükség van, pl: ha befejezte a mentést a szövegszetkesztô, vagy hasonló. A GetPriority() metódus pedig a form-on található combobox értékének megfelelôen adja vissza a kívánt prioritás értéket.

Lássuk konkrét megvalósítást:

procedure TThreadForm.SzamolButtonClick(Sender: TObject); begin   SzamolButton.Enabled:=False;   CloseButton.Enabled:=False;   Image1.Visible:=False;   Image2.Visible:=False;   if (StrToInt(EditX.Text) > 32767) or (StrToInt(EditX.Text) < 100)   then EditX.Text:='10000';   if (StrToInt(EditY.Text) > 32767) or (StrToInt(EditY.Text) < 100)   then EditY.Text:='10000';   ThreadX:=TSzamoloThreadX.Create(ThreadXLabel, StrToInt(EditX.Text));   if ComboBox1.Itemindex <> -1   then ThreadX.Priority:=GetPriority(ComboBox1.Itemindex)   else ThreadX.Priority:=tpNormal;   ThreadY:=TSzamoloThreadY.Create(ThreadYLabel, StrToInt(EditY.Text));   if ComboBox2.Itemindex <> -1   then ThreadY.Priority:=GetPriority(ComboBox2.Itemindex)   else ThreadY.Priority:=tpHigher;   ThreadX.OnTerminate:=ThreadDone;   ThreadY.OnTerminate:=ThreadDone;   ThreadsRunning:=2; end;

Mit is csinál a rutin:

  • A metódus elsô részében letiltjuk a SzamolButton gomot, hogy ne lehessen újra elindítani, míg be nem fejezôdik, míg a kilépés gombot a nem kívánt szál megszakítás érdekében tiltjuk le.
  • Van két Image osztályú objektumunk, mely majd a befejezéskor rak ki egy képet, hogy az adott szál már végzett.
  • Az ezután következô két feltétel megvizsgálja, hogy megfelelô-e az adott szálak ciklusának átadandó maximális értékek. 100 és 32767 között kell lennie.
  • Ezután hozzuk létre az X szálat:
ThreadX:=TSzamoloThreadX.Create(ThreadXLabel, StrToInt(EditX.Text));
  • Átadjuk a használandó Label-t, illetve a ciklus maximális értékét. Ezzel a szál valójában már el is indul.
  • Ezután a szál prioritásának beállítása következik. Ha nem adtunk meg prioritást (avagy a combobox-ban nem jelöltünk ki újat), akkor marad az alapértelmezett. X szál prioritása: tpNormal, Y szál prioritása: tpHigher.
  • Amennyiben kiválasztottunk a combobox-ban új értéket, akkor a GetPriority() metódus segítségével visszaadjuk a megfelelô típusú prioritást. A combobox elemei rendre: 0 és 6 között vannak ezekhez rendeli rendre a szükséges tpXXX alakú prioritást.
  • A GetPriority() metódus szolgáltatott prioritást átadjuk a szálnak, tehát meghatározzuk a szál prioritását. Egyben az egész:

  • if ComboBox1.Itemindex <> -1 then ThreadX.Priority:=GetPriority(ComboBox1.Itemindex) else ThreadX.Priority:=tpNormal;
     
  • Az Y szál létrehozása és prioritásának beállítása is hasonlóan történik:

  • ThreadY:=TSzamoloThreadY.Create(ThreadYLabel, StrToInt(EditY.Text));
    ifComboBox2.Itemindex <> -1
    thenThreadY.Priority:=GetPriority(ComboBox2.Itemindex)
    elseThreadY.Priority:=tpHigher;
     
  • Annyi kivétellel, hogy tpHigher prioritás értéket adunk alapesetben, illetve a megfelelô ciklus maximumot és Label-t állítjuk be.
  • Továbbá be kell állítani, hogy a fô form értesüljön a szál befejezésérôl, s ekkor saját rutint hajthasson végre:

  • ThreadX.OnTerminate:=ThreadDone; ThreadY.OnTerminate:=ThreadDone; ThreadsRunning:=2;
     
  • Végezetül a szálak számát adjuk meg, a könnyebb figyelés végett:

  • ThreadsRunning:=2;

Lássuk a ThreadDone() metódust:

procedure TThreadForm.ThreadDone(Sender: TObject); begin   Dec(ThreadsRunning);   MessageBeep(1);   if Sender = ThreadX then Image1.Visible:=True;   if Sender = ThreadY then Image2.Visible:=True;   if ThreadsRunning = 0 then   begin     SzamolButton.Enabled:=True;     CloseButton.Enabled:=True;   end; end;
 
  • Amikor ez a metódus meghívódik - tehát biztosan befejezôdött az egyik szám - csökkenti a futó szálak számát,
  • egy "csippanó" jelzést ad,
  • attól függôen, hogy melyik szál végzett - avagy hívta meg az OnTerminate esemény kezelôt - akkor az annak megfelelô befejezô képet rakja ki.
  • S, ha minden szál befejezte tevékenységét, akkor a kezelô gombokat elérhetôvé teszi.
(Érdemez még a BorderIcon-okat letiltani, vagy az applikáció OnClose eseményét figyelni, hogy rákérdezhessünk biztosan ki akar lépni: "fut még a háttérben egy munka".)

A program a szálak befejezése után:

Érdemes kipróbálni az egyes prioritás kombinációkat, meg lehet figyelni milyen hatással vannak egymásra, és a fô szálra a különbözô prioritásértékek. (A nagyobb prioritás különbségek eredményezhetik, hogy a másik szál (a kisebb prioritású) csak a nagyobb prioritású befejezése után jut elegendô végrehajtási idôhöz. A tpTimeCritical prioritás a legnagyobb prioritás, ha mindkét szál ezzel rendelkezik, akkor a fô szál már valószínűleg "nem jut szóhoz". Így érdemes odafigyelni az egyes szálak jó prioritásának beállítására.)

Egy kicsit látványosabb, barkácsolt példa 2

Másik példánk, a Borland Delphi-hez mellékelt (Copyright 1998 Borland Inc.), tradicionális thread példa. Három szál van, minden egyes szál egy tömböt rendez különbözô algoritmusokkal. Ez látványosabbnak látványos, de ugyanazt a gyakorlati megvalósítás menetet alkalmazza, így nem is taglaljuk.
 


 

Következô cikkünkben valószínűleg már adatbázis-kezeléssel foglalkozunk. Akit érdekel ez az új téma ajánlom figyelmébe adatbázis-kezelés című rovatunkat, mely leírja az adatbázis-kezelés alapjait és a legelterjedtebb leíró nyelvet, az SQL-t. (Ezen rovatunk már 4. részénél tart, az elôzô számokban megtalálhatóak a régebbi cikkek.) Az ebben taglalt alapfogalmakat ismertnek tekintjük.

A cikkben tárgyalt forráskódok és lefordított változatuk:
 

   Forráskód Lefordított EXE
Példa 1 Forráskód1.zip try_threads.exe
Példa 2 (Borland Delphi példa) Forráskód2.zip Thrddemo.exe