Umíme to s Delphi, 55. díl – vlákna a paralelní programování: vlákna a MDI

Dnes nás čeká další přísun informací o vláknech v Delphi. Nejprve si ukážeme, jak nastavovat vláknům prioritu a ověříme si, že priority mají značný význam. A potom společně vytvoříme skutečnou, pořádnou vícevláknovou aplikaci (architektury MDI).
Než se začneme zabývat dnešními tématy, zodpovíme otázku položenou v „námětu pro zvídavé uživatele“ v minulém dílu. Druhým možným výsledkem je číslo -200, protože při spouštění vláken není zaručeno, které vlákno se spustí jako první (a tedy které vlákno dříve vejde do kritické sekce). A pokud se nejprve spustí „odčítací“ vlákno, vypíše se výsledek –200 a teprve potom se spustí cyklus ve druhém vláknu.

Nastavování priority vláken

Chceme-li vytvářeným vláknům nastavovat priority, máme v zásadě dvě možnosti. Předpokládejme, že pracujeme s vlákny – objekty třídy TThread.

Způsob první: nastavení vlastnosti Priority

Velmi jednoduchým (a dostatečně funkčním) řešením je nastavení hodnoty vlastnosti Priority objektu vlákna. Obecně můžeme tuto vlastnost nastavit kdekoliv, obvykle se používá místo ihned za vytvořením tohoto objektu. Je zde však jeden problém – i když spíše rázu teoretického, v praxi se obvykle neprojeví.

Pomocí konstruktoru Create vytvoříme nové vlákno. Hned dalším příkazem mu nastavíme nějakou prioritu. Z toho, co jsme si o vláknech řekli, ovšem vyplývá, že operační systém přepíná mezi vlákny neznámou rychlostí a v neznámých okamžicích. Teoreticky je tedy možné, že po vytvoření nového vlákna systém přepne na jiné vlákno ještě předtím, než se provádění dostalo k nastavení vlastnosti Priority. V takovém případě nově vytvořené vlákno chvíli poběží s implicitní prioritou a teprve po chvíli se přepne na prioritu požadovanou. Nicméně znovu opakuji,že v „obyčejných aplikacích“ se není třeba tohoto problému příliš obávat. A navíc – snadno jej lze vyřešit tak, že vytvoříme vlákno pozastavené (parametr konstruktoru True), dalším příkazem nastavíme prioritu a dalším je rozběhneme (Resume):

NoveVlakno :=  TMyThread.Create(True); { vytvoří se, ale nespustí }

NoveVlakno.Priority := tpLower; { nastavení priority }
NoveVlakno.Resume; { spuštění vlákna }

Způsob druhý: předefinování konstruktoru Create

Čistší způsob spočívá v tom, že předefinujeme konstruktor Create třídy vlákna (tedy potomka třídy TThread). V tomto konstruktoru nastavíme požadovanou prioritu (nastavením hodnoty vlastnosti Priority) a v okamžiku, kdy se vlákno spustí, je již bezpečně priorita taková, jakou potřebujeme. Tento způsob si ukážeme v následující aplikaci.

Ukázková aplikace demonstrující priority vláken

Nyní vytvoříme aplikaci, pomocí které si ukážeme, že nastavování priority má skutečně podstatný význam. Princip aplikace bude následující: na formuláři se budou pohybovat dva kruhy (komponenty Shape). Každým kruhem bude pohybovat jedno vlákno. Podle toho, jakou prioritu bude dané vlákno mít, poběží příslušná kulička rychleji či pomaleji.

1. Vytvořte novou aplikaci, formulář nazvěte frmHlavni a umístěte na něj dvě komponenty Shape (Shape1, Shape2), tři tlačítka (btnPozastavit, btnObnovit, btnKonec).

2. Definujeme nový datový typ TPShape jako ukazatel na řídu TShape:

type
  TPShape = ^TShape;

3. Vytvoříme třídu vlákna TKruhVlakno. Protože chceme, aby obě vlákna (pohybující oběma kuličkami) pracovala podle téhož předpisu (nebudeme přece pro dvě pohybující se kuličky vytvářet dvě nové třídy vláken), musíme si k tomu třídu uzpůsobit. Jednak deklarujeme soukromý atribut Pozice, ve kterém budeme uchovávat aktuální pozici kuličky na formuláři. Dále deklarujeme soukromý atribut PKruh, který bude obsahovat ukazatel na kruh, kterým se má pohybovat. Při vytvoření vlákna pak musíme do tohoto atributu přiřadit ukazatel na jednu z komponent Shape na formuláři. Kvůli tomu, a kvůli korektnímu nastavení priority vytvářeného vlákna, také předefinujeme konstruktor Create.

type
  TKruhVlakno = class(TThread)
    private
      PKruh: TPshape;
      Pozice: Integer;
    protected
      procedure Execute; override;
      procedure VykresliKruh;
      constructor Create(PK: TPShape; Prior: TThreadPriority);
end;

4. Definujeme dvě globální proměnné pro dvě vlákna: Kruh1 a Kruh2.

var
  frmHlavni: TfrmHlavni;
  Kruh1, Kruh2: TKruhVlakno;

5. Předefinujeme konstruktor vlákna. Důležité je nejprve vyvolat konstruktor předka (tedy třídy TThread) s odpovídajícím parametrem – False znamená, že vlákno se po vytvoření ihned spustí. Nastavíme také atribut Pozice.

constructor TKruhVlakno.Create(PK: TPShape; Prior: TThreadPriority);
begin
  inherited Create(False);

  PKruh := PK;
  Priority := Prior;
  Pozice := PKruh.Left;
end;

6. Implementujeme metodu Execute. budeme testovat, jestli nám kruh nehodlá opustit formulář, v takovém případě jej pěkně vrátíme na začátek jeho dráhy. Všimněte si, že manipulujeme s atributem PKruh, který obsahuje ukazatel na jednu z komponent Shape. Podle této metody Execute tedy mohou běžet obě vlákna posouvající obě komponenty Shape. Metodu pro vykreslení komponent samozřejmě voláme pomocí metody Synchronize (důvod viz předchozí díly seriálu).

procedure TKruhVlakno.Execute;
var I: Integer;
begin
  repeat
    if Pozice > (frmHlavni.Width - PKruh.Width - 10) then
      Pozice := 2;

    for I := 0 to 1000000 do
      Pozice := Pozice + 1;

    Pozice := Pozice - 1000000;
    Synchronize(VykresliKruh);
  until Terminated;
end;

Poznámka: cyklus zvyšující hodnotu Pozice do milionu s následným odečtením je uveden, aby se tak simulovala alespoň nějaká akce, protože pak je teprve vidět, že priority skutečně fungují. Kdyby tento cyklus nebyl přítomen, oba kruhy by se pohybovaly úplně stejně, protože největší režii stojí logicky samotné překreslování (což je synchronizované a tedy vlastně sériové). Samotný kód vlákna by byl tak krátký (a časově nenáročný), že výsledek by vůbec neukazoval význam nastavování priorit!

7. Implementace metody pro vykreslení kruhu na nové pozici je poměrně jednoduchá. Důležité je v tomto případě pouze zavolání Application.ProcessMessages. Kdybychom tuto metodu nepoužili, byla by aplikace (při vyšších prioritách vláken) tak vytížená, že by se vůbec nedala ovládat.

procedure TKruhVlakno.VykresliKruh;
begin
  PKruh.Left := Pozice;
  Application.ProcessMessages;
end;

8. V obsluze OnCreate hlavního formuláře provedeme nezbytné inicializace a kromě nich také vytvoříme obě vlákna. Pro začátek jim oběma přiřadíme stejnou prioritu – tpNormal. Všimněte si také použití zděděné vlastnosti formuláře DoubleBuffered. Její hodnota True říká, že komponenta nebude překreslována přímo, ale s použitím pomocné bitmapy uložené v paměti. Důsledkem jsou sice větší paměťové nároky, ale hlavně eliminujeme problikávání formuláře.

procedure TfrmHlavni.FormCreate(Sender: TObject);
begin
  Self.Caption := `Priorita vláken`;
  Self.DoubleBuffered := True;
  btnPozastavit.Caption := `&Pozastavit`;
  btnObnovit.Caption := `&Obnovit`;
  btnKonec.Caption := `&Konec`;

  btnPozastavit.Enabled := True;
  btnObnovit.Enabled := False;

  Kruh1 := TKruhVlakno.Create(@Shape1, tpNormal);
  Kruh2 := TKruhVlakno.Create(@Shape2, tpNormal);
end;

9. V obsluze události OnDestroy hlavního formuláře obě vlákna zrušíme. Abychom je však mohli zrušit, musíme je nejprve zastavit, a protože zastavit je můžeme jedině doběhnutím jejich metody Execute, musíme je paradoxně úplně nejdříve rozběhnout (tedy pokud neběží).

procedure TfrmHlavni.FormDestroy(Sender: TObject);
begin
  if Kruh1.Suspended then Kruh1.Resume; // rozběhnout
  if Kruh2.Suspended then Kruh2.Resume;

  Kruh1.Terminate; // zastavit
  Kruh2.Terminate;

  Kruh1.Free; // zrušit
  Kruh2.Free;
end;

10. V obsluhách událostí OnClick tlačítek btnPozastavit a btnObnovit provedeme odpovídající operaci s vlákny a povolíme/zakážeme příslušná tlačítka:

procedure TfrmHlavni.btnPozastavitClick(Sender: TObject);
begin
  Kruh1.Suspend;
  Kruh2.Suspend;
  btnPozastavit.Enabled := False;
  btnObnovit.Enabled := True;
end;

procedure TfrmHlavni.btnObnovitClick(Sender: TObject);
begin
  Kruh1.Resume;
  Kruh2.Resume;
  btnPozastavit.Enabled := True;
  btnObnovit.Enabled := False;
end;

11. Zbývá ošetřit klepnutí na btnKonec. V něm pouze obě vlákna ukončíme (připomeňme, že metoda Terminate nastaví příznak vláken Terminated na True a protože jej opakovaně testujeme v metodě Execute, způsobí to ukončení běhu vlákna). Nakonec ukončíme aplikaci.

procedure TfrmHlavni.btnKonecClick(Sender: TObject);
begin
  Kruh1.Terminate;
  Kruh2.Terminate;
  if Kruh1.Suspended then Kruh1.Resume;
  if Kruh2.Suspended then Kruh2.Resume;
  Application.Terminate;
end;

Zkuste si aplikaci spustit a pozorujte, jak se chovají vlákna se stejnou prioritou – že v žádném případě neběží vždy stejně rychle. Pak v metodě FormCreate změňte prioritu jednoho z vláken například na tpLowest a sledujte, co to s kuličkami provede. Příjemnou zábavu:)

Klepněte pro větší obrázek

Vícevláknová MDI aplikace

Všechny aplikace, které jsme společně až dosud vytvářeli, měli jeden společný znak: používaly vždy jen jedno, maximálně dvě vlákna provádějící tutéž činnost. Vytvořili jsme třídu pro vlákno (potomka TThread) a jen jeden, maximálně dva objekty této třídy. To samozřejmě není to „pravé“ využití vláken: ortodoxní vícevláknovou aplikaci si mnozí představují tak, že existuje mnoho (teoreticky až nekonečně mnoho) „dělníků“, tedy vláken stejné třídy, kteří provádějí v zásadě tutéž činnost.

Poznámka: upřímně řečeno, v souvislosti s počtem dělníků nemusíme být zase tak velkorysí, za optimální maximum je považována (i přímo Borlandem) na jednoprocesorovém počítači 16 aktivních vláken na jeden proces.

Nyní společně takovou aplikaci vytvoříme. Výsledkem našeho snažení bude aplikace MDI (Multi Document Interface, podrobně viz 14. díl našeho seriálu). Po spuštění aplikace bude uživatel moci otvírat další a další nová okna, přičemž do každého okna bude samostatné vlákno vykreslovat úsečky náhodné pozice, barvy a délky.

Máme-li základní znalosti architektury MDI a základní znalosti problematiky vláken (obojí jste mohli získat v našem seriálu), nebude vytvoření této aplikace ničím nezvládnutelným.

1. Vytvořte novou MDI aplikaci (File – New…, zvolte kartu Projects a na ní MDI Application):

Klepněte pro větší obrázek

2. V otevřeném okně vyberte adresář (pokud možno dosud nepoužitý), ve kterém bude aplikace umístěna:

Klepněte pro větší obrázek

3. Vytvoří se již funkční MDI aplikace (v podstatě textový editor), nicméně my si ji trochu upravíme. Připomeňme si, že všechna klientská okna (všechny dokumenty), které se při běhu aplikace otvírají, jsou objekty třídy TMDIChild. Tato třída je deklarována a implementována v souboru ChildWin.PAS, který se také vytvořil. Proto si jej otevřeme (File – Open):

Klepněte pro větší obrázek

Poznámka: možná je rychlejší kliknout v sekci uses na CHILDWIN za současného držení klávesy Ctrl (máme totiž jistotu, že se otevře ten správný soubor).

4. Otevře se jednak příslušný formulář a jednak nová záložka v CodeEditoru. Formulář upravíme: odstraníme z něj komponentu Memo a nahradíme ji komponentou Image. Jméno komponenty Image změníme na imgOkno a její vlastnost Align nastavíme na alClient:

Klepněte pro větší obrázek

5. Nyní se již zaměříme na vlastní programování, konkrétně na soubor Childwin.PAS. Uvědomme si, co je zapotřebí provést. Každé nově vytvořené okno si vytvoří „své vlastní pracovní“ vlákno. Okno si musí zapamatovat, jaké vlákno pro něj bude pracovat. Vlákno také musí vědět, jakému oknu slouží – přesněji, do jaké komponenty Image bude vykreslovat výsledky svého snažení. Ukazatel na komponentu Image musíme vláknu prozradit ihned při jeho vytvoření. Při svém běhu bude vlákno generovat souřadnice začátku a konce úsečky, stejně jako její barvu. Vlákno bude modifikovat obsah vizuální komponenty (to zdůrazňuji proto, aby byla vidět nutnost vytvoření metody vlákna, kterou budeme volat pomocí Synchronize). Abychom mohli splnit všechnu tuto požadovanou funkčnost, vytvoříme deklaraci pomocného datového typu UkazImage (jako ukazatel na třídu TImage), a dále deklarujeme třídu TMDIChild (podle které poběží všechna vlákna). Upravíme také deklaraci třídy TMDIChild. Pokud vám předchozí popis není jasný, doporučuji přečíst si předchozí díly seriálu (pokud možno poslední čtyři), ve kterých najdete podrobný popis problematiky vláken. V následujících deklaracích si (kromě jiného) všimněte toho, že ve třídě TMDIChild musíme předefinovat konstruktor jejího předka (třídy TForm):

type
  UkazImage = ^TImage;

type TMDIVlakno = class(TThread)
  private
    PImage: UkazImage;
    X1, Y1, X2, Y2: Integer;
    Barva: TColor;

  protected
    constructor Create(PPImage: UkazImage);
    procedure Execute; override;
    procedure VykresliCaru;
end;


type
  TMDIChild = class(TForm)
    imgOkno: TImage;
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    constructor Create;
  private
      Vlakno: TMDIVlakno;
    { Private declarations }
  public
    { Public declarations }
  end;

6. Nyní, když máme důkladně rozmyšlenu funkčnost aplikace (která vyústila v deklarace tříd), bude již zbytek hračkou. Začneme implementací konstruktoru Create vlákna (třídy TMDIVlakno). Uvnitř tohoto konstruktoru musíme především vyvolat konstruktor předka (třídy TThread) se správným parametrem (False pro spuštění vlákna ihned po vytvoření). Pak jen přiřadíme atributu PImage vlákna hodnotu předanou v parametru – jde o ukazatel na komponentu Image, do které bude vlákno vykreslovat. Nastavíme také prioritu vlákna – protože nejde o žádnou kriticky důležitou činnost, bohatě si vystačíme s prioritou tpLowest.

constructor TMDIVlakno.Create(PPImage: UkazImage);
begin
  inherited Create(False);

  PImage := PPImage;
  Priority := tpLowest;
end;

7. Metoda vlákna Execute nás nepřekvapí:

procedure TMDIVlakno.Execute;
begin
  repeat
    X1 := Random(PImage.Width);
    Y1 := Random(PImage.Height);
    X2 := Random(PImage.Width);
    Y2 := Random(PImage.Height);
    Barva := Random($FFFFFF);

    Synchronize(VykresliCaru);
  until Terminated;
end;

8. Metoda vlákna VykresliCaru je také bezproblémová:

procedure TMDIVlakno.VykresliCaru;
begin
  PImage.Canvas.Pen.Color := Barva;
  PImage.Canvas.MoveTo(X1, Y1);
  PImage.Canvas.LineTo(X2, Y2);

  PImage.Repaint;
end;

9. Tolik ke třídě TMDIVlakno, je dokončena. Přesuneme se ke třídě TMDIChild. U ní, jak jsme si řekli, musíme nejprve předefinovat konstruktor (nesmíme zapomenout vyvolat konstruktor předka, třídy TForm, jinak se okno v žádném případě nevytvoří!). V tomto konstruktoru vytvoříme vlákno příslušné novému oknu a jako parametr mu předáme adresu (referenční operátor @) komponenty imgOkno:

constructor TMDIChild.Create;
begin
  inherited Create(Application);

  DoubleBuffered := True;
  Vlakno := TMDIVlakno.Create(@imgOkno);
end;

10. Jednou z posledních věcí, kterou musíme udělat, je ukončení a uvolnění vlákna při uzavření klientského okna. Metoda TMDIChild.FormClose je již implementována, proto jen do jejího těla připíšeme dva příkazy pracující s vláknem:

procedure TMDIChild.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Action := caFree;
  Vlakno.Terminate;
  Vlakno.Free;
end;

11. Aplikace je prakticky hotová! Poslední maličkostí, kterou musíme učinit, je jednoduchá změna metody TMainForm.CreateMDIChild v souboru Main.PAS. Přepněte se tedy do tohoto souboru (v Code Editoru pomocí záložky), najděte implementaci této metody a vypusťte z ní poslední řádku (která načítala a ukládala obsah komponenty Memo, již jsme odstranili, ze/do souboru). Dále odstraňte parametr z volání konstruktoru třídy TMDIChild (tento konstruktor jsme předefinovali bez parametrů, parametr Application předáváme až uvnitř tohoto konstruktoru při volání konstruktoru předka):

procedure TMainForm.CreateMDIChild(const Name: string);
var
  Child: TMDIChild;
begin
  { create a new MDI child window }
  Child := TMDIChild.Create;
  Child.Caption := Name;
end;

12. To je už skutečně všechno. Aplikaci uložte, zkuste si ji spustit a vytvořte si několik oken. Funguje? :)

Klepněte pro větší obrázek

Můžete si pogratulovat, právě jste vytvořili první důkladnou vícevláknovou aplikaci:) I když je samozřejmě pravda, že ve vhodně navržené aplikaci vůbec nevadí, pokud existuje jen jedna instance třídy vlákna. Nicméně asi uznáte, že takováhle aplikace, ve které navrhneme jednu třídu vlákna, a pak vytvoříme tolik vláken, kolik se nám zlíbí, vypadá hezky:)

Na závěr

Původně jsem si myslel, že téma vláken již uzavřeme. Protože však témata související s vlákny jsou takřka nevyčerpatelná, shledáme se s nimi za týden ještě jednou. Naposledy, slibuji:-). Povíme si, jak pracovat s vlákny bez třídy TThread, vysvětlíme si lokální data vláken... a to není rozhodně všechno. Víte třeba, jak ladit vícevláknový program? Standardně (pomocí bodů přerušení, krokování a watches) to asi nepůjde:-)
Diskuze (1) Další článek: Dražba počítače

Témata článku: Software, Programování, Nová metoda, Neznámá aplikace, Child, Nový typ, DEL, ALCL, Priority, Random, Díl, Nový způsob, Shape, Pozice, Nová komponenta, Druhá sekce, Změna dráhy, Resume, Správný parametr, Jednoduchá pozice, Code, Stejná metoda, Pracovní vlákno, Vlákno, Nové vlákno


Určitě si přečtěte

Není jen Flightradar: Našli jsme další aplikace pro sledování letadel, některé ukážou i víc

Není jen Flightradar: Našli jsme další aplikace pro sledování letadel, některé ukážou i víc

** 8 služeb pro sledování leteckého provozu ** Nejznámější je Flightradar24, ale alternativy leckdy prozradí více ** Letadla i v této pohnuté době čile létají a je co pozorovat

Karel Kilián | 14

Porovnání deseti cloudových disků: kam a za kolik uložit 100 GB, 1 TB a 10 TB dat?

Porovnání deseti cloudových disků: kam a za kolik uložit 100 GB, 1 TB a 10 TB dat?

** Zjistili jsme, kam do cloudu nejvýhodněji uložíte data ** Vytvořili jsme žebříček cen deseti cloudových úložišť ** Ceny se liší - často i velice výrazně!

Karel Kilián | 104


Aktuální číslo časopisu Computer

Megatest televizí do 25 000 Kč

Nejlepší herní klávesnice

Srovnání správců hesel

Jak upravit fotky pro tisk