-
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.
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.
-
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.
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:
-
Komponenty datového přístupu do databáze jsou vláknově bezpečné, pokud
každé vlákno má svoji komponentu Session. Jedinou výjimkou jsou
ovladače databáze Access (tyto ovladače nejsou vláknově bezpečné). Datové
databázové ovladače ale nejsou vláknově bezpečné.
-
Grafické objekty jsou vláknově bezpečné. Není tedy nutné používat hlavní
vlákno VCL pro přístup k TFont, TPen, TBrush, TBitmap,
TMetafile
a TIcon.
-
Zatímco objekty seznamů nejsou vláknově bezpečné, můžeme použít namísto
TList
jeho vláknově bezpečnou verzi TThreadList.
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.
-
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.
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.
-
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.
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.
-
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:
#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.
-
Zvolte si další řadící metodu a přidejte ji jako další vlákno do naší programové
jednotky.