THREADY
Thready ("pramínky") jsou objekty, umožňující paralelní zpracování procesu ve více souběžných větvích. Výhoda threadů je v tom, že paralelním zpracováním (a) dovoluje vyřešit úzká místa programu, (b) někdy zjednodušují algoritmus (někdy naopak...), (c) jsou přípravou pro multiprocesorové zpracování.
Vytvoření threadu
Ve většině běžných případů se použije třída objektů
TThread nebo třída z ní odvozená. Instance objektů se vytvářejí automaticky (jako u každé jiné třídy). Jen ve specielních případech, kdy je nezbytná kontrola velikosti zásobníku aplikace (stacku) anebo když je třeba řešit bezpečnostní otázky jednotlivých threadů, je možno k vytvoření použít funkce BeginThread. My se na cvičeních tímto případem nebudeme zabývat.Postup: Vytvoříme nový ob
jekt odvozený od TTHread tak, že v menu File-->New vybereme Thread Object. Zadáme jméno nového objektu a Delphi automaticky vytvoří příslušnou novou unitu. (Pozor, toto snad je jediné místo v Delphi, kde se před jméno třídy nenapíše "T" automaticky.) V unitě se automaticky připraví základní kostra.Inicializace threadu
Inicializace threadu je nepovinná. Provede se tak, že v nové třídě zděděné z TThread napíšeme nový konstruktor Create, ve kterém nainicializujeme potřebné části. Pozor na to, abychom nezapomněli zavolat původní konstruktor rodičovské třídy TThread! Příklad, jak může vypadat nový konstruktor, je zde:
constructor TMujThread.Create(CreateSuspended: Boolean);
begin
inheritedCreate(CreateSuspended);
Priority := tpIdle;
end;
Inicializace se nejčastěji použije k tomu, abychom
threadu přiřadili požadovanou prioritu. Provede se to tak, že jednoduše přiřadíme hodnotu do vlastnosti Priority. Přípustné hodnoty jsou:
tpIdle |
nejnižší priorita; proces běží jen tehdy, když je systém v klidovém stavu. Windows kvůli tomuto threadu nepřeruší žádný jiný thread. |
tpLowest |
|
tpLower |
|
tpNormal |
|
tpHigher |
|
tpHighest |
|
tpTimeCritical |
maximální priorita; použije se pro časově nejkritičtější a bezpečnostní akce. Toto nastavení spotřebuje většinu času procesoru pro obsluhu časově kritického threadu. |
Ukončení threadu
Často potřebujeme
thread, který se provede jen jednou a po provedení požadované akce zmizí. Nejjednodušší způsob ošetření takového threadu je, že nastavíme vlastnost FreeOnTerminate na hodnotu true. Naopak, když nechceme, aby se thread po vykonání akce sám zrušil (třeba když uživatel může spustit několik akcí paralelně, takže neustálé vytváření a rušení threadu by zdržovalo), nastavíme FreeOnTerminate na false.Činnost threadu
Činnost
threadu musí být obsažena v jeho metodě Execute. Na metodu Execute můžeme pohlížet jako by to byl samostatný program, který naše aplikace spustí - až na to, že všechny thready naší aplikace sdílí s ní společnou paměť a ostatní prostředky. Napsání metody Execute je trochu složitější než u normálního programu, protože musíme dbát, abychom si nepřepsali proměnné které mohou současně používat jiné thready. To proto, že komponenty Delphi nejsou psány pro paralelní běh v threadech. Jinými slovy, je naší starostí zaručit, že se komponenty uvnitř našeho threadu nebudou nežádoucím způsobem ovlivňovat.Zejména si musíme dát pozor na toto:Příklad
Dejme tomu, že napíšeme proceduru PushTheButton, která simuluje stisknutí tlačítka:
procedureTMujThread.PushTheButton;
begin
Button1.Click;
end;
Protože tato procedura existuje a bude používána jedině v rámci hlavního
threadu, je zcela legitimní, že se uvnitř této procedury volá metoda Click, která je součástí VCL knihovny (protože Button je VCL komponenta). Tuto proceduru ale (stejně jako každou jinou) musíme volat pomocí Synchronize, to znamená např. takto:procedure TMujThread.Execute;
begin
...
Synchronize(PushTheButton);
...
end;
Synchronize počká, až se hlavní VCL thread dostane do stavu, kdy obsluhuje frontu zpráv od Windows. V ten moment Synchronize spustí požadovanou metodu, protože v ten okamžik je její spuštění bezpečné. (To má, mimochodem, jedno úskalí - konzolové aplikace, tj. aplikace nepoužívající grafické rozhraní, neobsluhují frontu zpráv, a proto ke spuštění nikdy nedojde.)
Privátní proměnné
Kdybychom potřebovali proměnnou, která je dostupná ze všech procedur a funkcí
threadu, ale která je pro každou instanci threadu jiná, privátní, použijeme deklaraci threadvar namísto var. Této deklarace nelze použít ve funkcích a procedurách threadu a nelze ji aplikovat na proměnné typu funkce či procedura.Synchronizace
K synchronizaci threadů se (vedle již ukázané metody Synchronize) použijí ještě 3 další prostředky:
LockXY.Acquire; { vyloučení ostatních threadů }
try
Y := sin(X);
finally
LockXY.Release;
end;
Rád bych zdůraznil, že kritická sekce může fungovat jen při dobré kázni programátorů. Jinými slovy -
thread, který nepoužije metodu Acquire, se do kritické sekce normálně dostane, i když tam zrovna nemá co dělat, proto opomenutí zavolat Acquire a Release může mít fatální následky.Čekání na jiné thready
Potřebuje-li nějaký
thread počkat na ukončení jiného threadu, máme dvě možnosti.Čekání na ukončení threadu
Lze toho dosáhnout pomocí metody WaitFor. Tato metoda počká, až je určený thread úplně dokončen (buď tím, že je ukončena jeho Execute, nebo tím, že "spadne" následkem výjimky). Například tento fragment programu počká, až ListFillingThread naplní data do ThreadListu a teprve pak zahájí jeho zpracování, tzn. přístup k datům:
if ListFillingThread.WaitFor then
begin
with ThreadList1.LockList do
begin
for I := 0 to Count - 1 do
ProcessItem(Items[I]);
end;
ThreadList1.UnlockList;
end;
Ač to z této ukázky není na první pohled zřejmé,
WaitFor je funkce, která vrací funkční hodnotu. Touto funkční hodnotou je údaj, který zapíše ListFillingThread, tzn. ten thread, na který se čeká, do proměnné ReturnValue. Funguje to tedy tak, že ListFillingThread je odněkud vyvolán svou metodou Execute, uvnitř této metody Execute si zapíše hodnotu do ReturnValue a po čase skončí. Jakmile skončí, rozběhne se jiný doposud čekající thread a pomocí WaitFor se mu předá návratová hodnota, předtím uložená do ReturnValue.Pozor, WaitFor nikdy nepoužívejte na thread, který je synchronizován pomocí Synchronize!
Čekání na dokončení operace
Někdy je zbytečné čekat až na úplné dokončení
threadu, stačilo by dokončení určité operace. V takovém případě použijeme objekt TEvent. Vytvoříme jednu instanci tohoto objektu, přístupnou všem threadům. Jakmile thread dokončí sledovanou operaci, zavolá Event.SetEvent. Tím se nastaví signál, přístupný všem ostatním threadům, že operace je dokončena. Signál se naopak vymaže pomocí ResetEvent.Například, kdybychom měli čekat na ukončení několika threadů současně, nemohli bychom použ
ít WaitFor (protože nevíme, který z nich skončí poslední). Problém vyřešíme tak, že si zavedeme zvláštní proměnnou Counter, ve které si budeme ukládat počet právě běžících threadů. Jakmile nastane Counter=0, neběží žádný thread a můžeme vykonat nějakou akci. Například obsluha události OnTerminate by mohla čekat na dokončení všech threadů takto:procedureTDataModule.TaskThreadTerminate(Sender: TObject);
begin
...
CounterGuard.Acquire; { položím zámek na čítač }
Dec(Counter); { decrementuju čítač }
if Counter = 0 then
Event1.SetEvent; { signalizuju že to je poslední thread }
CounterGuard.Release; { uvolním zámek na čítač }
...
end;
Hlavní thread nainicializuje čítač Counter, spustí všechny thready a čeká, až jsou všechny dokončeny. Pozná to tak, že sleduje metodu WaitFor. Ta může nabývat následujících hodnot:
wrSignaled |
byl nastaven signál |
wrTimeOut |
vypršel nastavený čas, aniž by přišel signál |
wrAbandoned |
objekt byl destruován dřív, než vypršel čas |
wrError |
během čekání došlo k chybě |
V následujícím fragmentu je ukázáno, jak hlavní thread spustí sekundární thready, počká na jejich dokončení a poté pokračuje dál:
Event1.ResetEvent; { před spuštěním threadů vymazat }
for i := 1 to Counter do
TaskThread.Create(False); { vytvořit a spustit sekundární thready }
if Event1.WaitFor(20000) <> wrSignaled then
raise Exception; { chyba při nestandardním ukončení}
{ zda pokračuje hlavní thread, protože všechny sekundární byly ukončeny }
Kdybychom ve WaitFor použili jako parametr konstantu INFINITE, nikdy by nenastalo vypršení času. S touto možností ale je třeba nakládat velmi opatrně, protože při algoritmické chybě se thread může snadno dostat do nekonečné smyčky, a tak "zamrznout" celou aplikaci.
Žádost o ukončení threadu
Závěrem si ještě ukážeme případ zcela inverzní. Může nastat situace, kdy máme nějaký
thread a ostatní thready jej mohou požádat, aby se ukončil. Provede se to tak, že jakýkoliv jiný thread může zavolat metodu Terminate našeho threadu. Tím se mu zvenčí nastaví jeho vlastnost Terminared na hodnotu true a podle toho thread pozná, že má skončit:procedure TMujThread.Execute;
begin
while not Terminated do
DelejNeco;
end;