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ý objekt 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:

  1. Zamykání. Zamykání lze použít jen u objektů, které jej podporují. Takovým objektem například je TCanvas a TThreadList. Oba mají metody pro uzamčení (Lock, LockList) a pro odemčení (Unlock, UnlockList). Jakmile je objekt nějakým threadem uzamčen, žádný jiný thread do něj nemůže přistoupit, dokud zase nedojde k jeho odemčení. Zámky lze libovolně i vícenásobně vnořovat.
  2. Kritická sekce. Do kritické sekce může vstoupit jen jediný thread a dokud z ní nevystoupí, žádný jiný do ní vstoupit nemůže. Je to prostředek pro synchronizaci threadů, které nedovolují zamykání. Kritická sekce se používá tak, že vytvoříte jednu globální instanci (tj. pro celou aplikaci společnou) třídy TCriticalSection. Tento objekt má metody Acquire (pro vstup do kritické sekce) a Release (pro výstup z kritické sekce). Kritická sekce se typicky používá pro přístup (zápis) do sdílené paměti. Kdyby například aplikace měla objekt LockXY:TCriticalSection pro řízení přístupu ke sdíleným proměnným X a Y, tak by se typicky zapsalo
  3. LockXY.Acquire; { vyloučení ostatních threadů }

    try

    Y := sin(X);

    finally

    LockXY.Release;

    end;

  4. Vícenásobné čtení a exkluzívní zápis. Synchronizace pomocí kritické sekce je příliš přísná, protože dovoluje jen jedinému threadu, aby se dostal do kritické sekce. V mnoha praktických případech by ale stačilo, kdyby thready měly omezen přístup do kritické sekce jen pro zápis, zatímco pro čtení by mohlo v této sekci koexistovat více threadů současně. Příkladem může být přístup do paměti, kde není na závadu, bude-li současně několik threadů číst tatáž data. Tato situace je ošetřena pomocí objektu TMultiReadExclusiveWriteSynchronizer. Funguje podobně jako kritická sekce a také se podobně aplikuje, tzn. je třeba vytvořit jednu globální instanci tohoto objektu v paměti. Na rozdíl od kritické sekce ale má dvě dvojice metod, a sice BeginRead-EndRead pro čtení a BeginWrite-EndWrite pro zápis. Každý thread před použitím společné části paměti musí zavolat metodu pro čtení nebo metodu pro zápis podle toho, jestli hodlá data jen číst nebo i zapisovat.

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;