25. Používání vláken I
  1. C++ Builder poskytuje několik objektů, které usnadňují zápis vícevláknových aplikací. Vícevláknové aplikace jsou aplikace, které obsahují několik souběžných cest provádění. Při použití jednoho vlákna, program musí zastavit všechno provádění, když čeká na dokončení pomalého procesu (např. zpřístupnění souboru na disku, komunikaci s dalšími počítači nebo zobrazování multimédií). CPU čeká, dokud proces není dokončen. S více vlákny, naše aplikace může pokračovat v provádění dalších vláknen, když jedno vlákno čeká na výsledek pomalého procesu.

  2. Chování programu může být často organizováno do několika paralelních procesů, které pracují nezávisle. Každý z těchto paralelních procesů může být prováděn souběžně pomocí jednoho vlákna. Jednotlivým vláknům lze přiřadit prioritu, čím určíme jak mnoho času CPU bude vlákno používat. Pokud systém, na kterém běží náš program, má několik procesorů, můžeme zvýšit výkonnost rozdělením práce do několika vláken a spouštět je současně na různých procesorech.
    Operační systém Windows NT podporuje pravé multiprocesorové zpracování, pokud to umožňuje použitý hardware. Windows 98 toto pouze simuluje.
  3. K reprezentaci prováděného vlákna v naší aplikaci většinou použijeme objekt vlákna. Objekty vlákna zjednodušují zápis vícevláknových aplikací zaobalením nejčastějších požadavků na vlákna. Objekty vláken neumožňují ovládat bezpečnostní atributy nebo velikost zásobníku našeho vlákna. Abychom to mohli provést, musíme použít funkci API Windows CreateThread.

  4. K použití objektu vlákna v naši aplikaci, musíme vytvořit potomka TThread. Pro vytvoření potomka TThread zvolíme File | New a na stránce New vybereme Thread Object. Dále jsme dotázáni na jméno třídy (není zde automaticky přidáváno T před jméno) pro náš nový objekt vlákna. C++ Builder vytvoří zdrojový a hlavičkový soubor k implementaci vlákna. Automaticky vygenerovaný zdrojový soubor obsahuje kostru kódu pro náš nový objekt vlákna. Pokud jej nazveme TMojeVlakno, pak soubor má obsah:
    #include <vcl.h>
    #pragma hdrstop
    #include "Unit1.h"
    #pragma package(smart_init)
    __fastcall TMojeVlakno::TMojeVlakno(bool CreateSuspended)
        : TThread(CreateSuspended)
    {
    }
    //---------------------------------------------------------------
    void __fastcall TMojeVlakno::Execute()
    {
        //---- Zde umístíme kód vlákna ----
    }
    Musíme doplnit kód konstruktoru a kód metody Execute.
    Konstruktor použijeme k inicializaci naší nové třídy vlákna. Můžeme zde přiřadit implicitní prioritu vlákna a určit, zda vlákno bude automaticky uvolněno po dokončení provádění.
    Priorita určuje, kolik prostředků vlákno získá, když operační systém plánuje využití času CPU pro všechna vlákna v naši aplikací. Vyšší prioritu použijeme pro vlákna zpracovávající kritické úlohy a nižší prioritu pro vlákna provádějící ostatní úkoly. Pro určení priority našeho vlákna nastavíme vlastnost Priority. Je zde sedm možných hodnot (viz následující tabulka):
     
    Hodnota Priorita
    tpIdle Vlákno je prováděno pouze, když systém je nečinný. Windows nepřerušuje provádění ostatních vláken k provedení těchto vláken.
    tpLowest Priorita vlákna je dva body pod normálem.
    tpLower Priorita vlákna je jeden bod pod normálem.
    tpNormal Vlákno má normální prioritu.
    tpHigher Priorita vlákna je jeden bod nad normálem.
    tpHighest Priorita vlákna je dva body nad normálem.
    tpTimeCritical Vlákno získá nejvyšší prioritu.

    Následující kód ukazuje konstruktor vlákna nejnižší priority, které je prováděno na pozadí (nepřerušuje ostatní aplikace):
    __fastcall TMyThread::TMyThread(bool CreateSuspended):
                          TThread(CreateSuspended)
    {
      Priority = tpIdle;
    }
    Často aplikace provádí jisté vlákno pouze jednou. V tomto případě je nejvhodnější, když objekt vlákna uvolní sám sebe. To nastane, když vlastnost FreeOnTerminate je nastavena na true. Jestliže objekt vlákna reprezentuje úlohy aplikace, které jsou prováděny několikrát (např. v reakci na akci uživatele nebo příchodu externí zprávy), pak můžeme zvýšit výkonnost odložením vlákna pro opětovné použití (namísto jeho zrušení a opětovného vytvoření). To provedeme nastavení vlastnosti FreeOnTerminate na false.
    Metoda Execute je činnost našeho vlákna. Můžeme zde určit, jak vlákno bude chápáno naší aplikací, mimo sdílení stejného procesového prostoru. Zápis vlákna je nepatrně složitější než zápis samostatného programu, neboť se musíme ujistit, že nepřepisujeme paměť používanou jinými vlákny v aplikaci. Na druhé straně, protože vlákno sdílí stejný procesový prostor s ostatními vlákny, můžeme použít sdílenou paměť pro komunikaci mezi vlákny.
    Když používáme objekty z hierarchie objektů C++ Builderu, pak jejich vlastnosti a metody nezajišťují bezpečné vlákno. Tj. zpřístupňováním vlastností nebo prováděním metod mohou být prováděny některé akce, používající paměť, která není chráněna před akcemi z ostatních vláken. Hlavní vlákno VCL je nastaveno mimo přístup k objektům VCL. Jedná se o vlákno, které zpracovává zprávy Windows přijaté komponentami v naší aplikaci.
    Jestliže všechny objekty zpřístupňují své vlastnosti a provádějí své metody v jednom vlákně, pak nemohou nastat problémy s interakcí našich objektů s jinými. K použití hlavního vlákna vytvoříme samostatnou funkci, která provádí požadované akce. Tuto samostatnou funkci voláme z metody Synchronize vlákna. Např.
    void __fastcall TMyThread::PushTheButton(void)
    {
      Button1->Click();
    }
    void __fastcall TMyThread::Execute()
    {
      ...
      Synchronize((TThreadMethod)PushTheButton);
      ...
    }
    Synchronize čeká pro hlavní vlákno VCL na vstup cyklu zpráv, a potom provede předanou metodu.
    Ne vždy je nutno používat hlavní vlákno VCL. Některé objekty jsou vláknově bezpečné. Když víme, že objekt je vláknově bezpečný, pak není nutné použít metodu Synchronize, což zvyšuje výkonnost, neboť není nutno čekat na vstup vlákna do cyklu zpráv. Metodu Synchronize není nutno použít v následujících situacích:

    Naše metoda Execute a funkce jí volané, mají své vlastní lokální proměnné, stejně jako libovolné jiné funkce C++. Tyto funkce mohou také přistupovat ke globálním proměnným. Globální proměnné poskytují mechanismus pro komunikaci mezi vlákny. Někdy, ale potřebujeme použít proměnné, které jsou globální pro všechny funkce běžící ve vláknu, ale nejsou sdíleny dalšími instancemi stejné třídy vlákna. Potřebujeme tedy deklarovat vláknové lokální proměnné. Provedeme to přidáním modifikátoru __thread k deklaraci proměnné. Např.
    int __thread x;
    deklaruje proměnnou typu int, která je soukromá pro každé vlákno v aplikaci, ale je globální v každém vláknu. Modifikátor __thread může být použit pouze s globálními a statickými proměnnými. Nemůže být použit s ukazateli nebo funkcemi. Programový prvek, který vyžaduje běhovou inicializaci nebo finalizaci nemůže být deklarován s __thread. Následující deklarace vyžaduje běhovou inicializaci a je tedy nedovolená:
    int f();
    int __thread x = f();    // nedovoleno
    Vytvoření instance třídy s uživatelem definovaným konstruktorem nebo destruktorem vyžaduje běhovou inicializaci a je tedy nedovolený:
    class X  {

       X( );
       ~X( );
    };
    X __thread myclass;   // nedovoleno
    Naše vlákno začíná běžet při volání metody Execute a pokračuje, dokud Execute neskončí. Používá se model, ve kterém vlákno provádí specifické úkoly a když je provede, pak skončí. Někdy ale aplikace potřebuje provádět vlákno dokud není splněna nějaká externí podmínka.
    Můžeme umožnit ostatním vláknům signalizovat, že vlákno ukončilo provádění a to testováním vlastnosti Terminated. Když jiné vlákno vyžaduje ukončení našeho vlákna, pak volá metodu Terminate. Terminate nastavuje vlastnost Terminated našeho vlákna na true. Naše metoda Execute může vlastnost Terminated použít např. takto:
    void __fastcall TMyThread::Execute()
    {
      while (!Terminated)
        ProvedeniAkce();
    }
    Můžeme centralizovat vyčišťující kód, který je proveden, když naše vlákno ukončí provádění. Před dokončením práce vlákna vzniká událost OnTerminate. Vyčišťující kód umisťujeme do obsluhy události OnTerminate a tak zajistíme, že bude vždy proveden. Obsluha události OnTerminate již není spuštěna jako část našeho vlákna. Je spuštěna v kontextu hlavního vlákna VCL naší aplikace. V obsluze OnTerminate tedy není možno používat lokální proměnné vlákna a můžeme bezpečně přistupovat ke všem komponentám a objektům VCL.

  5. Když zapisujeme kód, který běží při provádění našeho vlákna, musíme předpokládat chování dalších vláken, které mohou být spuštěny současně. V jistých případech musíme zabránit dvěma vláknům v použití stejného globálního objektu nebo proměnné ve stejné době. Kód v jednom vlákně může záviset na výsledcích úloh prováděných dalšími vlákny.

  6. Pro zabránění ostatním vláknům v přístupu ke globálním objektům nebo proměnným můžeme blokovat provádění ostatních vláken, dokud naše vlákno nedokončí operaci. Lze použít uzamykání objektů nebo kritickou sekci.
    Některé objekty mají zabudovaný zámek k zabránění používání dalšími vlákny. Např. objekty plátna (TCanvas a jeho potomci) mají metodu Lock, která zabraňuje dalším vláknům v přístupu k plátnu, dokud není volána metoda Unlock. Objektová hierarchie také obsahuje vláknově bezpečný seznam objektů, TThreadList. Volání TThreadList::LockList vrací seznam objektů, který je také blokován před dalšími vlákny dokud není volána metoda UnlockList. Volání TCanvas::Lock nebo TThreadList::LockList může být bezpečně vnořováno. Uzamčení není uvolněno, dokud není odemčeno poslední uzamčení na současném vláknu.
    Pokud objekt nemá zabudovaný zámek, pak můžeme použít kritickou sekci. Kritická sekce pracuje jako brána, která umožňuje v jednom čase vstup pouze jednoho vlákna. Pro použití kritické sekce vytvoříme globální instanci TCriticalSection. TCriticalSection má dvě metody: Acquire (která zabraňuje dalším vláknům v provádění sekce) a Release (odstraňuje blokování).
    Každá kritická sekce je přiřazena ke globální paměti, kterou má chránit. Každé vlákno, používající tuto globální paměť, musí nejprve použít metodu Acquire k zajištění toho, aby další vlákna ji nemohla použít. Po ukončení, vlákno volá metodu Release a další vlákna mohou přistoupit ke globální paměti voláním Acquire. Vlákna, která ignorují kritickou sekci a přistupují ke globální paměti bez volání Acquire, mohou způsobit problémy souběžného přístupu. Např. předpokládejme aplikaci, která má kritickou sekci pLockXY blokující přístup ke globálním proměnným X a Y. Každé vlákno, které používá X nebo Y musí použít kritickou sekci takto:
    pLockXY->Acquire(); // uzamknutí pro ostatní vlákna
    Y = sin(X);
    pLockXY->Release();
    Když používáme objekty z hierarchie objektů VCL, použijeme hlavní vlákno VCL k provedení našeho kódu. Použití hlavního vlákna VCL zajišťuje, že objekty přistupují k paměti, která může být použita objekty VCL z jiných vláken, nepřímo. S hlavním vláknem VCL jsme se již seznámili. Tento problém je možno řešit i pomocí lokálních proměnných vlákna o  kterých jsme již také mluvili.
  7. Pokud naše vlákno musí čekat na další vlákna k dokončení nějaké úlohy, pak můžeme říci, že u našeho vlákna je dočasně potlačeno provádění. Můžeme čekat na kompletní dokončení jiného vlákna nebo čekat na signál od jiného vlákna informující, že byla dokončena nějaká úloha.

  8. Pro čekání na dokončení provádění jiného vlákna používáme metodu WaitFor tohoto jiného vlákna. WaitFor nekončí dokud toto jiné vlákno není dokončeno a to dokončením jeho metody Execute nebo vznikem výjimky. Např. následující kód čeká, dokud jiné vlákno nezaplní seznam objektů vlákna před zpřístupněním seznamu našemu vláknu:
    if (pListFillingThread->WaitFor())
    {
      for (TList *pList = ThreadList1->LockList(), int i = 0;
           i < pList->Count; i++)
        ProcessItem(pList->Items[i]);
      ThreadList1->UnlockList();
    }
    Nevolejte metodu WaitFor vlákna, které používá Synchronize pro hlavní vlákno VCL. Pokud hlavní vlákno VCL má volání WaitFor, pak ostatní vlákna nezískají vstup cyklu zpráv a Synchronize nikdy neskončí. Objekty vláken detekují tento stav a generují výjimku EThread ve vlákně, které volá Synchronize.
    V předchozím příkladě, je seznam prvků zpřístupněn pouze tehdy, když metoda WaitFor indikuje, že seznam byl úspěšně zaplněn. Tato vrácená hodnota musí být přiřazena metodou Execute vlákna na které čekáme. Protože vlákna která volají WaitFor potřebují znát výsledek prováděného vlákna a ne kód, metody Execute nevracejí žádnou hodnotu. Místo toho Execute nastavuje vlastnost ReturnValue. ReturnValue je pak vrácena metodou WaitFor když je volána jiným vláknem. Vrácené hodnoty jsou celá čísla. Jejich význam je určen naší aplikací.
    Můžeme také čekat na dokončení nějaké operace jiného vlákna a ne na kompletní dokončení provádění vlákna. K tomuto účelu použijeme objekt události. Objekt události (TEvent) musí být vytvořen v globálním rozsahu (musí být viditelný ve všech vláknech). Když vlákno dokončí operaci, na kterou čekají jiná vlákna, je voláno TEvent::SetEvent. SetEvent spustí signál a jiná vlákna mohou testovat zda operace byla dokončena. K vypnutí signálu použijeme metodu ResetEvent.
    Např. následující kód zapisuje obsah seznamu řetězců do souboru a signalizuje dokončení zápisu do souboru. Vypnutím signálu před zápisem souboru dosáhneme toho, že ostatní vlákna nemohou k souboru přistupovat během zápisu.
    void __fastcall WriteTheStrings(void)
    {
      StringList1->SaveToFile("Example.txt");
    }
    void __fastcall TWritingThread::Execute()
    {
      ...
      Event1->ResetEvent(); // vypnutí signálu
      Synchronize((TThreadMethod)WriteTheStrings);
      Event1->SetEvent();
     ...
    }
    Ostatní vlákna testují signál voláním metody WaitFor. WaitFor čeká specifikovaný časový interval na nastavení signálu a vrací jednu hodnotu z následující tabulky:
     
    Hodnota Význam
    wrSignaled Signál události byl nastaven
    wrTimeout Specifikovaný čas vypršel před nastavením signálu.
    wrAbandored Objekt události byl zrušen před vypršením časového intervalu.
    wrError Výskyt chyby během čekání.

    Následující kód testuje zda soubor zapisovaný v předchozím příkladě je možno bezpečně číst. Časový interval je nastaven na 500 milisekund:
    if (Event1->WaitFor(500) == wrSignaled)
      // čtení řetězců
    Jestliže nechceme zastavit čekání na událost vypršením času, předáme metodě WaitFor parametr INFINITE. Při použití INFINITE musíme být opatrní, protože naše vlákno bude stále čekat na nastavení signálu.
    Spouštěním objektů vláken se budeme zabývat v následující kapitole.


  9. Nyní začneme vytvářet vícevláknovou aplikaci. Předpokládejme, že chceme demonstrovat postup a rychlost řazení pole hodnot podle velikosti několika různými metodami. Porovnání rychlosti řazení různými metodami provedeme nejlépe tak, že tyto jednotlivé metody naprogramujeme a spustíme současně (toto mám umožní vícevláknová aplikace). Musíme vytvořit vícevláknový objekt, který je potomkem třídy TThread. V našem případě vytvoříme třídu TSortThread. Začneme vývoj nové aplikace (zvolíme File | New Application). Pro vytvoření vícevláknového objektu zvolíme File | New a na stránce New vybereme Thread object. Tím vytvoříme novou programovou jednotku pro uložení vícevláknového objektu (musíme zadat jméno vytvářené třídy; použijeme TSortThread). Do této jednotky doplníme deklaraci naší třídy (a jejich potomků pro jednotlivé metody řazení) podle následujícího výpisu (jednotku přejmenujeme na SortThd.cpp). Hlavičkový soubor jednotky bude obsahovat:

  10. #ifndef SortThdH
    #define SortThdH
    #include <ExtCtrls.hpp>
    #include <Graphics.hpp>
    #include <Classes.hpp>
    #include <System.hpp>
    //-------------------------------------------------------------------
    extern void __fastcall PaintLine(TCanvas *Canvas, int i, int len);
    //-------------------------------------------------------------------
    class TSortThread : public TThread
    {
    private:
     TPaintBox *FBox;
     int *FSortArray;
     int FSize;
     int FA;
     int FB;
     int FI;
     int FJ;
     void __fastcall DoVisualSwap(void);
    protected:
     virtual void __fastcall Execute(void);
     void __fastcall VisualSwap(int A, int B, int I, int J);
     virtual void __fastcall Sort(int *A, const int A_Size) = 0;
    public:
     __fastcall TSortThread(TPaintBox *Box, int *SortArray,
         const int SortArray_Size);
    };
    //------------------------------------------------------------------
    class TBubbleSort : public TSortThread
    {
    protected:
     virtual void __fastcall Sort(int *A, const int A_Size);
    public:
     __fastcall TBubbleSort(TPaintBox *Box, int *SortArray,
         const int SortArray_Size);
    };
    //------------------------------------------------------------------
    class TSelectionSort : public TSortThread
    {
    protected:
     virtual void __fastcall Sort(int *A, const int A_Size);
    public:
     __fastcall TSelectionSort(TPaintBox *Box, int *SortArray,
         const int SortArray_Size);
    };
    //----------------------------------------------------------------
    class TQuickSort : public TSortThread
    {
    protected:
     void __fastcall QuickSort(int *A, const int A_Size, int iLo, int iHi);
     virtual void __fastcall Sort(int *A, const int A_Size);
    public:
     __fastcall TQuickSort(TPaintBox *Box, int *SortArray,
         const int SortArray_Size);
    };
    //-----------------------------------------------------------------
    #endif
    Funkce PaintLine bude použita pro zobrazování postupu řazení. Následuje výpis programové jednotky:
    #include <vcl.h>
    #pragma hdrstop
    #include "sortthd.h"
    void __fastcall PaintLine(TCanvas *Canvas, int I, int Len)
    {
      TPoint points[2];
      points[0] = Point(0, I*2+1);
      points[1] = Point(Len, I*2+1);
      Canvas->Polyline(EXISTINGARRAY(points));
    }
    //----------------------------------------------------------------
    __fastcall TSortThread::TSortThread(TPaintBox *Box, int *SortArray,
        const int SortArray_Size) : TThread(False)
    {
      FBox = Box;
      FSortArray = SortArray;
      FSize = SortArray_Size + 1;
      FreeOnTerminate = True;
    }
    //---------------------------------------------------------------------
    /* Jelikož DoVisualSwap používá komponenty VCL nemůže být nikdy volána
       přímo vláknem. DoVisualSwap musí být volána předáním metodě
       Synchronize (DoVisualSwap bude provedeno hlavním vláknem VCL). */
    void __fastcall TSortThread::DoVisualSwap()
    {
      TCanvas *canvas;
      canvas = FBox->Canvas;
      canvas->Pen->Color = TColor(clBtnFace);
      PaintLine(canvas, FI, FA);
      PaintLine(canvas, FJ, FB);
      canvas->Pen->Color = clRed;
      PaintLine(canvas, FI, FB);
      PaintLine(canvas, FJ, FA);
    }
    //-----------------------------------------------------------
    /* VisusalSwap usnadňuje používání DoVisualSwap. Parametry jsou
       překopírovány do proměnných instance, čímž je zpřístupníme hlavnímu
       vláknu VCL když provádí DoVisualSwap  */
    void __fastcall TSortThread::VisualSwap(int A, int B, int I, int J)
    {
      FA = A;
      FB = B;
      FI = I;
      FJ = J;
      Synchronize(DoVisualSwap);
    }
    //----------------------------------------------------------
    /* Metoda Execute ve volána při spouštění vlákna */
    void __fastcall TSortThread::Execute()
    {
      Sort(FSortArray, FSize-1);
    }
    //-----------------------------------------------------------
    __fastcall TBubbleSort::TBubbleSort(TPaintBox *Box, int *SortArray,
      const int SortArray_Size):TSortThread(Box, SortArray, SortArray_Size)
    {
    }
    void __fastcall TBubbleSort::Sort(int *A, int const AHigh)
    {
      int I, J, T;
      for (I=AHigh; I >= 0; I--)
        for (J=0; J<=AHigh-1; J++)
          if (A[J] > A[J + 1])
          {
            VisualSwap(A[J], A[J + 1], J, J + 1);
            T = A[J];
            A[J] = A[J + 1];
            A[J + 1] = T;
            if (Terminated) return;
          }
    }
    //--------------------------------------------------------------
    __fastcall TSelectionSort::TSelectionSort(TPaintBox *Box,
      int *SortArray, const int SortArray_Size)
      : TSortThread(Box, SortArray, SortArray_Size)
    {
    }
    void __fastcall TSelectionSort::Sort(int *A, int const AHigh)
    {
      int I, J, T;
      for (I=0; I <= AHigh-1; I++)
        for (J=AHigh; J >= I+1; J--)
          if (A[I] > A[J])
          {
            VisualSwap(A[I], A[J], I, J);
            T = A[I];
            A[I] = A[J];
            A[J] = T;
            if (Terminated) return;
          }
    }
    //--------------------------------------------------------------
    __fastcall TQuickSort::TQuickSort(TPaintBox *Box, int *SortArray,
      const int SortArray_Size)
      : TSortThread(Box, SortArray, SortArray_Size)
    {
    }
    void __fastcall TQuickSort::QuickSort(int *A, int const AHigh, int iLo,
      int iHi)
    {
      int Lo, Hi, Mid, T;
      Lo = iLo;
      Hi = iHi;
      Mid = A[(Lo+Hi)/2];
      do
      {
        if (Terminated) return;
        while (A[Lo] < Mid) Lo++;
        while (A[Hi] > Mid) Hi--;
        if (Lo <= Hi)
        {
          VisualSwap(A[Lo], A[Hi], Lo, Hi);
          T = A[Lo];
          A[Lo] = A[Hi];
          A[Hi] = T;
          Lo++;
          Hi--;
        }
      }
      while (Lo <= Hi);
      if (Hi > iLo) QuickSort(A, AHigh, iLo, Hi);
      if (Lo < iHi) QuickSort(A, AHigh, Lo, iHi);
    }
    void __fastcall TQuickSort::Sort(int *A, int const AHigh)
    {
      QuickSort(A, AHigh, 0, AHigh);
    }
    V této jednotce jsou jednotlivé řadící metody deklarovány jako třídy odvozené od TSortThread. Pokuste se pochopit, jak jednotlivé objekty vláken pracují. Pokračovat ve vývoji této aplikace budeme v následující kapitole.
  11. Zvolte si další řadící metodu a přidejte ji jako další vlákno do naší programové jednotky.
25. Používání vláken I