Deprecated: mysql_connect(): The mysql extension is deprecated and will be removed in the future: use mysqli or PDO instead in /home/www/novacisko.cz/subdomains/bredy/init.php on line 11

Warning: mysql_connect(): Headers and client library minor version mismatch. Headers:50562 Library:100020 in /home/www/novacisko.cz/subdomains/bredy/init.php on line 11

Warning: Cannot modify header information - headers already sent by (output started at /home/www/novacisko.cz/subdomains/bredy/init.php:11) in /home/www/novacisko.cz/subdomains/bredy/index.php on line 38
Multithreading v C++ (ve Win32) - Bredyho blog - Ondřej Novák
Bredyho blog - Ondřej Novák

Multithreading v C++ (ve Win32)

Představení knihovny pro multithreading v C++. Jedná se čistě o objektový návrh multithreadingu inspirovaný Javou. Mnoho programátorům může usnadnit práci s vícero vlákny ve WinApi.


Knihovna MultiThread

Knihovna MultiThread je psána tak, aby bylo možné kdykoliv převést tuto knihovnu na jinou platformu a přitom aby knihovna nabízela většinu služeb, které nabízí operační systém Windows.

Inspirací pro vznik knihovny MultiThread je rozhraní pro vlákna v jazyce Java. Toto rozhraní je pěkně navržené a velice flexibilní.

Jak netvořit vlákna

Starší programátoři a zejména programátoři ve Windows (a v MFC) si na tvorbu vláken oblíbili některou ze tři následujících funkcí.

  1. CreateThread (WinAPI funkce, jenž vytváří vlákno)
  2. _beginthread (funkce z process.h, jenž navíc inicializuje CRT knihovnu)
  3. AfxBeginThread (MFC funkce, jenž navíc inicializuje MFC).

Funkce CreateThread má svůj jistý význam, protože WinAPI není objektové rozhraní, není vlastně jiný způsob, jak vytvářet thready.
Tuto funkci však nechme některé low-level knihovně, na úrovni C++ by se neměla používat.

Funkce _beginthread je jen takový hack, vlastně řešící nedostatek WinAPI. CRT knihovnu je nutné inicializovat a připravit na vznik
nového vlákna. I tuto funkci nechme některé low-level knihovně, na úrovni C++ by se neměla používat.

Funkce 'AfxBeginThread' je opět něco, co se v MFC nepovedlo. MFC, které se snaží balit neobjektové WinAPI do objektů zcela
nepochopitelně nabádá programátora používat klasický neobjektový přístup. Programátor sice může použít třídu CWinThread, ale tato
třída je hodně závislá na MFC. A vlákno prochází částmi kódu, které mají být přenositelné (a stačí aby měly být přenositelné ven z MFC).

V každém případě platí, že programátoři by si měli odvyknout používat klasický přístup přes pomocnou statickou funkci. V době objektového programování je jediným rozumným řešením děděním abstraktního interface. To sebou nese (jak si následně ukážeme) řadu výhod.

Jak tvořit vlákna

Knihovna MultiThread má k dispizici dva nástroje

  1. Rozhraní 'IRunnable'
  2. Třídu 'Thread' (a jejího předka 'ThreadBase')

Třída IRunnable

Třída - interface 'IRunnable' je velmi primitivní

class IRunnable
{
public:
virtual ~IRunnable(void)=0 {}
virtual unsigned long Run()=0;
};

Všechny třídy, jež dědí IRunnable mohou být tzv. spustitelné. Jakmile existuje nějaká implementace rozhraní IRunnable, lze takovou implementaci spustit jako vlákno.

Vlastní kód vlákna se zapisuje do funkce Run. Předpokládá se, že parametry se předávají jako member proměnné třídy která IRunnable implementuje.

V okamžiku, kdy máme k dispozici instanci třídy jenž dědí IRunnable, můžeme vytvořit 'Thread' a ten spustit

class MyRunnable: public IRunnable
{
....
};

MyRunnable myRunnable;
Thread thr1(&myRunnable);
thr1.Start(); //zde vznikne nový thread

Instance třídy která dědi IRunnable i vlastní instance třídy Thread musí existovat po celou dobu běhu vlákna.
(instance třídy IRunnable může být zničena před ukončením funkce Run, pokud je Thread dynamicky alokován a má být zničen po skončení běhu vlákna, použijte třídu 'ThreadDyn')

Více vláken v jedné třídě

Potřebuje-li třída implementovat více vláken (více druhů vláken), může využit vnitřní třídy. Jeden příklad za všechny, jenž toto demonstruje:

class ThreadedClass
{
class ThrRunnable;
friend class ThrRunnable;
//vyuziti vnitrni tridy predstavujici vlakno.
class ThrRunnable: public IRunnable
{
int _threadType;
ThreadedClass *_outer;
public:
ThrRunnable(ThreadedClass *outer, int type):_threadType(type),_outer(outer) {}
unsigned long Run()
{
_outer->Run(_threadType);
delete this;
return 0;
}
};

public:
void Run(int type); //Volani Run s parametrem typu threadu

void StartThreadExample()
{
Thread *thr=new ThreadDyn(new ThrRunnable(this,5)); //alokace vlakna s parametrem 5
thr->Start(); //spusteni vlakna
//vlakno se samo znici jakmile dobehne.
}
};
Třída Thread a ThreadBase

Zdědit přímo třídu 'Thread' má oproti IRunnable jiné půvaby, ale i nevýhody.

  • Protože třída 'Thread' předpokládá práci s IRunnable, je lepší dědit z třídy 'ThreadBase', která je abstraktní, neboť nemá nadefinovanou funkci Run. Způsob dědění třídy 'ThreadBase' je tedy stejné jako dědění z IRunnable
  • Výhodou dědění 'ThreadBase' je ten, že vzniká jen jediná instance.
  • Nevýhodou je závislost na 'ThreadBase', takže nová třída předpokládá využití výhradně s thready. Také si instance s sebou nosí veškerá data potřebná pro režii vlákna.

V třídě ThreadBase lze dále předdefinovat následující funkce

  • Kromě funkce Run také funkci 'Init'. Spouští se před spuštěním Run a je vyhrazena k dodatečným inicializacím, které je potřeba vykonat před spuštěním vlákna. Nedoporučuje se psát výkonný kód do funkce Init. Funkce vrací stav, zda inicializace proběhla úspěšně ('true') nebo neúspěšně ('false'). V případě neúspěchu se vlákno nespustí a řízení je ihned předáno funkci Done.
  • Funkce 'Done' musí provést destrukci veškerých inicializovaných dat během Init nebo během činnosti Run. Jako poslední akce funkce Done může být destrukce sama sebe (delete this), pokud instance vlákna je alokovaná dynamicky a má se po skončení vlákna dealokovat.

Třída ThreadBase dědí IRunnable.

class MyThread1: public ThreadBase
{
....
};

class MyThread2: public ThreadBase
{
....
public:
void Done(bool initFailed) {delete this;}
};

MyThread1 myThread1
myThread1.Start(); //zde vznikne nový thread

MyThread2 *myThread2=new MyThread2;
myThread2->Start(); //dynamicky alokovaný thread
Řízení běhu vlákna
Start vlákna
Funkce 'Start' způsobí založení a start vlákna. Dokud vlákno není spuštěno, fyzicky neexistuje a operační systém nevede vlákno v evidencni. V této fázi tedy nelze provádět operace, které vyžadují běžící vlákno. Volitelným parametrem je parametr bool, pokud je true, vlákno se spustí, ale bude pozastavené, dokud se neuvolní příkazem 'Resume'
thread.Start() // Start vlákna
thread.Start(true) // Start pozastavený
Ukončení vlákna
Na ukončení vlákna neexistuje v rozhraní Thread příkaz. Vlákno se ukončí v okamžiku, kdy dokončí svou práci a vrátí se z funkce Run. Pokud musí jedno vlákno počkat na dokončení jiného vlákna, může použít funkci 'Join'. Při čekání na více vláken naráz z výhodou použijeme funkci 'JoinMulti'.
Pozastavení vlákna
Vlákno lze kdykoliv pozastavit příkazem 'Suspend'. To se však obecně nedoporučuje, pokud není předem známo, ve kterém místě bude vlákno pozastaveno. Je-li potřeba vlákno pozastavit na konkrétním bodě, je mnohem lepší použít synchronizačních tříd. Pozastavené vlákno neběží, nezabírá tedy žádný čas procesoru. Nicmeně stále zabírá prostředky OS a ten jej bere v patrnosti.
Uvolnění vlákna
Pozastavené vlákno lze uvolnit příkazem 'Resume'. Přitom platí, aby vlákno bylo skutečně uvolněno, musí být počet Resume stejný s počtem Suspend volaných na totéž vlákno. Pokud je vlákno spuštěno pozastavené, připočtěte jeden Suspend navíc.
Návratová hodnota

Každé vlákno při ukončení předává návratovou hodnotu. Jakmile vlákno doběhne, lze návratovou hodnotu vyzvednou pomocí funkce 'ThreadBase::GetExitCode()'. Podmínkou je, že funkce IsRunning vrací false (a vlákno tedy bylo aspoň jednou spuštěno)

Fibery

Fibery jsou speciálním druhem vlákna. V jednom okamžiku smí pracovat pouze jedno Fiber-vlákno. Ostatní Fiber-vlákna jsou pozastavena. Jakmile běžící vlákno uvolní jiné vlákno, pozastaví se.

Slovní obrat "v jednom okamžiku" je zde vztažen na jedno Thread-vlákno. Každý Thread-vlákno může obsahova libovolné množství Fiber-vláken, minimálně však právě jedno.

Práce s Fiber třídou se neliší od práce od práce s ThreadBase. Opět je třeba založit potomka třídy Fiber a nadefinovat funkci Run. Třída Fiber je potomkem rozhraní IRunnable.

Fiber má k dispozici následující operace

  • Start
  • Resume
  • Stop
  • Yield
Start
Spustí fiber. Pokud je parametrem hodnota StartNormal, novému Fiberu předá řízení a uspí aktuální fiber. Pokud je parametrem hodnota STartSuspended, Fiber se pouze založí, ale program pokračuje normálně.
Resume
Jakmile je zavolána funkce Resume (a tato funkce musí být volána na objekt jiného Fiberu, než je aktuální), předá se řízení oslovenému fiberu a aktuální fiber se pozastaví. Oslovený Fiber pokračuje dalším příkazem za naposledy volaným příkazem Yield nebo Resume (v rámci jeho kontextu).
Stop
Na rozdíl od threadu, fibery lze kdykoliv ukončit zavoláním funkce Stop. Jen pozor na kontext zásobníku. Pokud fiber alokoval nějaké instance tříd na zásobníku a pak je takto uvolněn, destruktory těchto tříd se nezavolají. Fiber může být normálně ukončen tak, že sám dospěje na konec funkce Run.
Yield
Funkce pracuje podobně jako Resume. Rozdíl je v tom, že funkce Yield musí být volána pouze na instanci, která spravuje aktuální fiber. Funkce Yield se podívá, z kterého fiberu byl spuštěn aktuální fiber a tomuto fiberu předá řízení (a tím aktuální fiber pozastaví).

Funkce Resume pracuje trošku jako volání a funkce Yield pracuje jako návrat z volání. Rozdíl je v tom, že oslovený fiber nezačíná na začátku funkce Run, ale pokračuje od místa, kde byl naposledy přerušen. Při používání Resume je nutné si uvědomit, že neexistuje něco jako rekuze. Pokud fiber1 zavolá fiber2 a ten zpětně zavolá fiber1, nepozná fiber1, zda se tak stalo voláním nebo návratem.

Jak bylo řečeno výše, v rámci threadu může existovat libovolné množství fiberů, minimálně však jedno. Takže i samostatné vlákno je fiberem, tím prvním, který zakládá další fibery. Aby existovala nějaká instance představující ten první fiber, existuje třída 'MasterFiber'. Instanci této třídy je nutné vytvořit jako první, než se začne pracovat s fibery a musí existovat tak dlouho, dokud existuje nějaký další fiber v rámci threadu.

'MasterFiber' lze ovládat stejně jako ostatní fibery. 'Nedoporučuje se' používat na MasterFiber funkci Stop. Nezapomínejme, že thread se ukončí tehdy, když fiber dokončí funkci Run. Pokud se MasterFiber zastaví funkcí Stop, už nikdy se nedokončí metoda Run threadu.

Ideální místo pro alokaci 'MasterFiber' instance je automatická alokace v zásobníku threadu, jenž má pracovat s fibery.

Synchronizace

V okamžiku, kdy vlákno musí měnit proměnnou, jenž může být přístupná jiným vláknům, je potřeba přístup k proměnné synchronizovat. Jinými slovy, je nutné, aby vlákno získalo výhradní přístup k proměnné, čímž zaručí, že žádné jiné vlákno nebude po dobu přístupu k proměnné s proměnnou pracovat. Při získávání výhradního přístupu (zamykání) je nutné si uvědomit následující podmíky

  1. K zamknutí určitého proměnné je potřeba použít stejný typ synchronizace pro všechny vlákna. Typ synchronizace je odvozen od typu proměnné, kterou zamykáme.
  2. Z vyjímkou třídy Synchronize musí platit i to, že instance synchronizační třídy zamykající určitou proměnnou musí být pro danou proměnnou stejná. Nelze v jednom vlákně použít zámek1 a v druhém vlákně zámek2, pokud jsou to různé instance stejného typu synchronizace
  3. Zamykat proměnnou je nutné při každém přístupu, i když se jedná jen o jednoduchou operaci zjištění stavu. Výjímečně lze vynechat zámek u proměnných, jejichž velikost v bitech je menší nebo rovna šířce sběrnice procesoru a tato proměnná je správně zarovnána. Z toho vyplývá, že zamykat není třeba při čtení proměnných typu int, long, shot, char nebo float pokud aplikace běží na 32-bitovém stroji. Naopak při čtení proměnné double je zámek potřebný, protože zápis do proměnné tohoto typu může být rozdělen na dva samostatné zápisy.
Interlocked operace

Interlocked operace jsou nejjednodušší a nejjrychlejší a defacto atomické zámky, které nabízí samotný procesor. Tyto operace často řeší problém typu read-modify-write, kdy hrozí, že během fáze modify jiné vlákno obsah proměnné změní, avšak zápisem se tato nová hodnota přepíše původní.

  • Interlocked operace provedou operaci read-modify-write v jednom kroku. Během této operaci nikdy nedojde k přepnutí procesoru.
  • Interlocked operace také zamykají sběrnici procesoru, takže ani žádný jiný procesor nesmí během operace měnit paměť.
  • Žádné vlákno nečeká (zde je myšleno čekání nějakou čekací funkcí - viz dále) na dokončení interlocked operace. V jednoprocesorovém systému totiž nedojde k přepnutí (viz první bod). V multiprocesorovém systému signalizuje procesor během této operace výhradní přístup na sběrnici, takže ostatní procesory, které chtějí pracovat se sběrnicí musí pár taktů počkat.

'Operace k dispozici'

InterlockedIncrement
přičtení jedničky
InterlockedDecrement
odečtení jedničky
InterlockedExchange
Výměna obsahu proměnné
InterlockedCompareExchange
Výměna obsahu proměnné, pokud proměnná obsahuje zadanou hodnotu
InterlockedAdd
Přičtení hodnoty k proměnné

Operace 'InterlockedExchange' a 'InterlockedCompareExchange' vrací původní hodnotu, ostatní operace vrací novou hodnotu.

Knihovna MultiThread zabaluje tyto funkce do dvou vhodných tříd

SyncInt
Představuje proměnnou typu int, která má většinu operací definovaných pomocí Interlocked operací.
SyncPtr<type>
Představuje proměnnou typu pointer, který má většinu operací definovaných pomocí Interlocked operací.

Více informací o těchto třídách naleznete v dokumentaci

Důležitá poznámka:

SyncInt x;
...
if (x==5) {x=6;...} //spatne
...
if (x.SetValueWhen(5,6)) {...} //spravně

Operace porovnej-změn musí být interlocked.

Univerzalni synchronizace

Všechny následující synchronizační operace jsou složitější, vyžadují další datové struktury, zpracování synchronizace stojí nějakou režii a v případě kolize musí zastavovat jednotlivá vlákna.

Univerzální synchronizace je v MultiThread knihovně představována třídou Synchronized. Použití třídy demonstruje následující příklad:

AnObject GObject;


{
Synchronized ss1(&GObject);
GObject.NejakaFunkce();
GObject.JinaFunkce();
}

Třída Synchronized zamyká určitou adresu v paměti. V C++ platí, že každý objekt musí být identifikován adresu. Pokud tedy dvě vlákna zamykají stejný objekt, zamykají také stejnou adresu. Parametrem konstruktoru Synchronized může být nejen adresa objektu, ale také adresa libovolného jiného datového typu. Počas existence nějaké instance Synchronized je adresa zamknuta - to neznamená, že by nebyl možný přístup na jiné adresy, ale je potřeba hlídat aby při přístupu na ty adresy, které patří zamknutému objektu byla zamknuta ta adresa, která objekt identifikuje.

Třída Synchronized pracuje na velmi jednoduchém principu. V jednom okamžiku nesmí v celé aplikaci existovat více než jedna jediná instance této třídy s tím konkrétním parametrem (ve skutečnosti může, ale tuto instanci musí vytvořit stejné vlákno, které vytvořilo první instanci). Na významu a typu parametru nezáleží, musí se jedna o ukazatel, který se přetypovává na 'void *'. Význam tohoto ukazatele třída neintepretuje.

Výhoda třídy Synchronized je zejména v tom, že není třeba vytvářet pomocné datové struktury v objektech, které je třeba zamykat. Implementace třídy si vytváří tyto informace sama, a ukládá je do rychlých hashovacích tabulek.

Nevýhody této třídy jsou v lehce náročnější implementaci, což v časově kritických situacích nemusí být výhodné a také velká jednoduchost implementace může trpět jistými neduhy. Neexistuje fronta čekajících vláken, takže často může docházet k vyhladovění, pokud se jeden objekt zamyká velice často.

'Ideální použití:' Menší pravděpodobnost kolizí, veliké množství zamykatelných objektů.

Kritická sekce

Kritická sekce využívá všude tam, kde je potřeba uzamknout kus krátkého kódu pracující s určitými daty. Předpokládá se, že tento kód se provádí velice často, avšak méně často dochází ke kolizím. Pokud však ke kolizi dojde, kritická sekce ji vyřeší.

Třída 'CriticalSection' poskytuje dvě základní operace

Lock
zamknutí kritické sekce
Unlock
odemknutí kritické sekce

V rámci jednoho vlákna lze volat Lock vícekrát. Aby se pak kritická sekce odemkla, je třeba Unlock zavolat tolikrát, kolikrát bylo voláno Lock.

Výhodou kritické sekce je rychlost. Kritická sekce je z části implementována pomocí Interlocked operací. Pokud je kritická sekce zamykána a nedochází ke kolizi s jiným vláknem, omezí se operace zamknutí jen na několik jednoduchých Interlocked operací. Při zapnuté optimalizaci překladu se funkce Lock a Unlock přepíší do kódu a ušetří se čas voláním funkce. Pro víceprocesorové platformy lze kritickou sekci nastavit tak, aby při velmi malých sekcí kódu bylo čekání implementováno smyčkou, což může být často rychlejší, než zastavování a spouštění vláken pomocí prostředků OS.

Nevýhodou kritické sekce je neexistence front čekajících vláken a tedy nebezpečí vyhladovění vlákna, pokud ke kolizím dochází často. Na rozdíl třeba od Synchronized, instance kritické sekce musí existovat po dobu existence proměnné, která se kritickou sekcí chrání a samotná instance není malá, zabere cca 20 bajtů paměti a při kolizi i nějaké prostředky OS.

Mini-kritická sekce

Knihovna MultiThread se snaží řešit jednu nevýhodu kritických sekcí a to velikost. Plně aktivní kritická sekce zabere 20bajtů paměti a nějaké zdroje operačního systému. Mnohem často ale potřebujeme uzamknout část kódu nebo data na krátký okamžik. Vyloučit vliv ostatních vláken z nějakého rychlého výpočtu. V místech, kde nelze použít 'interlocked' operace a presto je treba udrzet zamek v minimální velikosti bez alokace zdrojů v OS.

Třída 'MiniCriticalSection' zabírá pouhé 4 bajty. Má k dispozici funkce Lock a Unlock stejně jako Kritická sekce. Rozdíly jsou tu dva:

  1. 'MiniCriticalSection' lze uzamknout pouze jednou. Rekurzivní uzamykání není podporováno, takže každé vlákno smí zavolat pouze jedenkrát Lock a k němu jedenkrát Unlock. Pokud zavolá Lock dvakrát za sebou, je to považováno za chybu.
  2. Pokud dojde ke kolizi (kritická sekce je uzamčena), nelze vlákno jednoduše zastavit. Pokud je předepsáno čekání, vlákno čeká ve smyčce a opakovaně testuje stav kritické sekce. Implementace se zároveň snaží předcházed vyhladovění tím, že se opakovaně vzdává svého 'time-slice' ve prospěch jiných vláken, čímž může urychlit odemčení kritické sekce. Toto čekání však i přes všechna opatření může vést k zahlcení procesoru a snížení jeho výkonu (vlákno stále běží, sdílí procesor s jinými vlákny). Proto je vhodné mini-kritickou sekci používat jen v tam, kde pravděpodobnost kolize je malá a zámek není držen dlouho.

Problém popsaný v bodě 1) řeší třída 'MiniCriticalSectionR'. Ta začleňuje do instance další 4 bajty v podobě čítače vnoření. Čítač pak slouží k počítání množství Lock a Unlock, aby k odemčení došlo ve správný okamžik.

Problém popsaný v bodě 2) nelze řešit pomocí 'MiniCriticalSection' a je nutné použít 'CriticalSection'

IBlocker

Blocker je nástroj na zastavování vlákna a synchronizaci s nějakou událostí. Příklady událostí:

  • Uvolnění nějakého výhradně zamknutého prostředku
  • Ukončení procesu
  • Ukončení jiného vlákna
  • Oznámení události z jiného vlákna
  • Dosažení určitého času
  • Příchod nějaké důležité zprávy v určitém čase.

...atd

Rozhraní IBlocker unifikuje přístup k všem synchronizačním nástrojům (Kromě třídy Synchronized a Interlocked operacím).

Pokud se vlákno rozhodně čekat na nějakou výše uvedenou událost, osloví příslušnou instanci rozhraní IBlocker pomocí funkce 'Acquire'. Následně se vlákno zastaví a čeká na zadanou událost. Před čekáním může nastavit timeout, který způsobí, že čekání se ukončí, pokud událost nenastane v zadaný čas.

Jakmile událost nastala, blocker čekající vlákno spustí a vlákno pokračuje ve své čínnosti. Některé typy blockerů vyžadují, aby je vlákno informovalo, že již vykonalo veškeré operace související s událostí a již se chce věnovat jiné činnosti. V tom případě vlákno musí zavolat funkci 'Release'. Volat 'Release' se doporučuje i tam, kde to nemá až takový význam (například při čekání na ukončení jiného processu - process již skončil a volání Release nemá na tento stav žádný vliv). Pokud tato operace nemá opodstatnění, blocker ji prostě ignoruje. Avšak vlákno může být využito v jiné situaci, kdy bude muset čekat na jiný blocker, který 'Release' bude vyžadovat.

'Typy blockerů'

EventBlocker
Jedno vlákno chce informovat jiné vlákno, že nastala událost. Pomocí funkcí 'Block' a 'Unblock' lze blocker nastavovat do stavu blokovaný nebo neblokovaný. Závislé vlákno pak buď projde přes Acquire, nebo se zastaví. EventBlocker se dá nastavit do 4 čtyř režimů práce, viz popis konstruktoru.
MutexBlocker
MutexBlocker pracuje stejně jako kritická sekce. Používá se však spíše tam, kde je třeba zamykat velké kusy kódu a kde často dochází ke kolizím. Mutex udržuje fronty čekajících vláken, takže nemůže docházet k vyhladovění. Na každé čekající vlákno se dostane podle pořadníku.
SemaphoreBlocker
Semafor pracuje podobně jako Mutex, rozdíl je v tom, že zamykaný prostředek není zamykán exclusivně pro jedno vlákno, lze nastavit maximální počet vláken, které v daném okamžiku moho s prostředkem pracovat. Všechna další vlákna musí čekat (příklad - čekárna a ordinace. V ordinaci jsou tři lékaři, takže tři pacianti mohou být vyšetřováni souběžně, všichni ostatní musí čekat v čekárně).
ProcessBlocker
Tento blocker blokuje všechna vlákna dokud určitý proces běží. Jakmile proces skončí, blocker se odblokuje. Pak již nelze blocker zablokovat. (Protože blocker podporuje operátor přiřazení, lze mu přiřadit jiný process, na který lze pak čekat).
ThreadBlocker
Tento blocker blokuje všechna vlákna dokud nějaké vlákno běží. Jakmile vlákno skončí, blocker se odblokuje. Pak již nelze blocker zablokovat. (Protože blocker podporuje operátor přiřazení, lze mu přiřadit jiné vlákno, na které lze pak čekat).
Poznámka čekat na vlákno v knihovně MultiThread lze pomocí operace Join. Pokud je třeba čekat pomocí Blockeru, je možné zavolat operaci GetBlocker. Pokud instance vlákna zaniká s vláknem, nelze tento blocker testovat, doporučuje se použít ThreadBlocker, který je platný i po zániku vlákna a jeho instance.
CriticalSectionBlocker
Tento blocker implementuje zamykání kritické sekce. Blocker lze využít všude tam kde se očekává objekt CriticalSection, ale i tam, kde se očekává objekt kompatibilní s rozhraním IBlocker.
SleepBlocker
Tento blocker implementuje kontrolované čekání. Vlákno které zavolá Acquire bude prostě čekat zadaný pořet milisekund. Jiný význam blocker nemá. Tento blocker k čekání nepoužívá čekací funkce (viz níže)
ThreadBase_InfoStruct
Tato třída je interní pro knihovnu a navenek není známa její definice. Používá se ve funkci 'ThreadBase::GetBlocker' a vnější "svět" s ní pracuje jako s rozhraním IBlocker. Její činnost je stejná jako ThreadBlocker, s tím rozdílem, že instance spolu se zánikem instance ThreadBase.
WinMessageBlocker
Blocker je určen pro aplikace WinAPI. Blocker se zablokuje vlákno do té doby, než dorazí nějaká zpráva z OS. Zprávy lze filtrovat zadaným filtrem. Tento blocker k čekání nepoužívá čekací funkce (viz níže)

Aby bylo možné IBlocker používat v templatech na zamykání, obsahuje rozhraní dvě další funkce, jenž jsou jen synonymem pro existující funkce.

Lock
Synonymum pro Acquire
Unlock
Synonymum pro Release

Třída 'BlockerSimpleBase' nabízí funkci 'AcquireMulti' a umožňuje čekat na více blockerů, jenž jsou potomky této třídy. Čekání je atomické, a pokud je potřeba čekat na více blockerů současně, doporučuje se této funkce využít. Lze čekat na všechny dohromady, nebo na "aspoň jeden". Využití například při čekání na více události, kdy se potřebujeme rozhodnout podle typu události.

TLS (Thread Local Storage)

Thread Local Storage je prostor pro uložení privátních informací přístupních jen aktuálně běžícímu vláknu.

'Proměnné obecně rozlišujeme'

Globální
Jsou to všechny proměnné, jenž jsou přístupně všem vláknům, ať už jsou to proměnné globálně deklarované, nebo proměnné dynamicky alokované, na jež se lze dostat pomocí odkazů z globálních proměnných. Přístup všech vláken zavádí nutnost synchronizace při práci s proměnnými.
Lokální
Jsou všechny proměnné alokované v zásobníku a přístupné jen té funkci, kde jsou deklarované. Tyto proměnné jsou zpravidla přístupné jen tomu vláknu, který k běhu využívá právě ten zásobník, v němž jsou proměnné alokované. Protože žádné další vlákno nemá přístup k těmto proměnným, synchronizace není nutná
Globální v rámci vlákna (v TLS)
Tyto proměnné mají zvláštní postavení, protože vystupují jako globální, ale každé vlákno má svou vlastní kopii hodnoty. Změna hodnoty v této proměnné se projeví pouze v rámci aktuálního vlákna (které změnu provedlo) a neovlivní proměnnou pro jiná vlákna. I tady není nutná synchronizace při přístupu.

Ve většině případech si vystačíme s lokálními proměnnými. Jsou však situace, kdy si některou hodnotu musíme odložit do nějaké globální proměnné, protože náš program bude hodnotu této proměnné potřebovat v nějaké funkci, zanořené kdesy hluboko, kam by se hodnota proměnné velice těžko dostávala. Pokud použijeme globální proměnnou, musíme synchronizovat přístup k ní a vlastně zajistit, aby se její hodnota nezměnila (jiným vláknem) do okamžiku, než ji budeme potřebovat. Ideální je bv tomto případě použit proměnnou v TLS.

V knihovně MultiThread je TLS implementováno třemi způsoby.

  1. Samotné vlákno má přístup ke své instanci pomocí metodt 'GetCurrentRunnable'. Výsledem operace je ukazatel na rozhraní IRunnable, který je nutné staticky nebo dynamicky přetypovat na ukazatel požadovaného typu. Díky tomu máme k dispozici ukazatel na instanci, jenž spravuje právě běžící vlákno.
  2. Pokud je třeba v určitém místě vytvořit proměnnou v TLS a původní třída která vlastní vlákno s ní nepočítá (například je sdílená různými projekty a nelze ji měnit ani dědit), může vlákno použít třídu 'RunningThread', zdědit ji a její instanci alokovat v zásobníku. Během existence této třídy bude funkce GetCurrentThread vracet ukazatel na instanci této třídy. Původní instance je pak přístupná přes metodu 'RunningThread::GetPreviousInstance()'. Vlákno si tak může vytvořit prostor pro další proměnné v TLS a k těm pak přistupovat. Třída 'RunningThread' nabízí i více možností, například dočasně změnit některé vlastnosti aktuálního vlákna, atd.
  3. Knihovna MultiThread dále nabízí třídu 'Tls' jenž umožňuje deklarovat proměnné jakoby globálně, a přesto v každém vlákně bude mít proměnná jinou hodnotu. Třídou Tls lze deklarovat pouze proměnné typu ukazatel:
Tls<int> GlobalInt;

void TestFunct()
{
int i=10;
GlobalInt=&i;
SubFunct();
}

void SubFunct()
{
int p=*GlobalInt;
printf("Value is: %d\n",p);
}

V příkladu nahoře se deklaruje ukazatel na int v Tls. Funkce TestFunct je volána v rámci nějakého vlákna. Deklaruje proměnnou i hodnotu 10 a ukazatel na tuto proměnnou uloží do GlobalInt proměnné. Pak volá funkci SubFunct, které z nějakých důvodů nemůže tuto hodnotu předat parametrem. Funkce SubFunct si přečte ukazatel z GlobalInt a vyzvedne si hodnotu, se kterou dále pracuje. Tento příklad je Thread-Safe, jiné vlákno může v daném okamžiku pracovat na stejném místě, avšak v GlobalInt bude mít uloženou svou kopii ukazatel.

Používání Tls je často univerzálnější, ale přináší nevýhody. Přístup do Tls není tak rychlý jako přístup do klasické globální proměnné. Tls prostor je omezený (v operačním systému Windows98 má pouze 64 položek. Doporučené použití je ukládat do Tls pouze ukazatele na větší struktury nebo třídy. Ukazatel si vždy vyzvednout jednou a pak přistupovat k proměnným příslušné instance.

Další nevýhodou je, že při skončení vlákna je obsah Tls zahozen. Nelze tedy do Tls ukládat ukazatele na dynamicky alokované instance, pokud není jasné, kde bude instance zničena. V okamžiku, jakmile vlákno skončí a instance není zničená, dojde k memory-leaku. Pokud místo Tls použijeme proměnné instance typu IRunnable, máme jistoru, že o zničení této instance se postará jiné vlákno, nebo se instance sama zničí, jakmile vlákno skončí.

O tom co je TLS a jaké má omezení v operačním systému Windows

Doporučení
Jako TLS používejte přednostně member proměnné třídy zděděné z 'Thread' přístupné přes funkci 'GetCurrentRunnable'. Nelze-li třídu změnit ani zdědit, použijte dědění třídy 'RunningThread'. Nelze-li použít ani tento přístup, pak teprve použijte třídu Tls.

Priorita threadu

Při konstrukci vlákna lze určit výchozí prioritu.

ThreadBase(Priority priority, size_t initStack=0);

Seznam priorit

PriorityIdle
Doporučuje se pro déle běžící procesy v pozadí
PriorityLow
Doporučuje se pro výpočty v pozadí, jenž se mají vykonat v rozumném čase, ale nesměji způsobovat zpomalení prostředí
PriorityNormal
Doporučuje se pro vlákna prostředí a výpočty, které netrvají déle než pár sekund
PriorityAboveNormal
Doporučuje se pro rychlé reakce na některou událost
PriorityHigh
Doporučuje se pro reakce na stavy hardware
PriorityHighest
Doporučuje se pro reakce na neodkladné stavy, velice rychle vykonané operace, a řízení v realném čase.

Vlákna s nižší prioritou jsou zastavena, pokud existuje vlákno z vyšší prioritou, které nečeká na žádnou událost. Windows však v náhodných intervalech dává možnost některému vláknu s nižší prioritou, aby nedocházelo k vyhladovění.

Prioritu vlákna lze za běhu měnit příkazem SetPriority. Toto lze dělat i v případě, že vlákno ještě nebylo spuštěno.

Čekací funkce

Velice často se stává, že vlákno musí čekat na nějakou událost, například během synchonizace. Standardní čekání je v knihovně MultiThread implementování pomocí funkcí operačního systému. Ty zajistí, aby během čekání mohl procesor vykonávat jiná vlákna.

Ne ve všech případech se však hodí k čekání použít standardní čekání. Příkladem jsou například vlákna, která musí být neustále připravena zpracovávat zprávy operačního systému (typicky UI vlákna), nebo vlákna, která během čekání mohou být vyrušena nějakou prioritní akcí, jenž musí vykonat (alterable).

Třída 'ThreadBase' obsahuje virtuální funkci

virtual bool WaitFunction(ThreadWaitInfo &info) 

Funkce je platformově závisla. Vyžaduje znalost struktury 'ThreadWaitInfo'. Tato struktura je skrytá pro aplikace, které mají být platformově nezávislé. Windows verze je uložena ve waitFunctions_win.h

Aby nebylo nutné psát implementaci pro nejběžnější typy vláken, jsou ve třídě ThreadWaitFn k dispozici tři nejčastější implementace čekacích funkcí.

DefaultWait(ThreadWaitInfo &waitInfo)
Výchozí čekání, během čekání není vlákno rušeno
UIWait(ThreadWaitInfo &waitInfo)
Čekání UI vlákna, čekání je rušeno příjmem a zpracováním všech důležitých zpráv z OS tak aby edocházelo deadlocku.
AlterAbleWait(ThreadWaitInfo &waitInfo)
Čekání Alterable vlákna, čekání je rušeno příjmem a zpracováním APC požadavků

Implementace pro jinou platformu

Knihovna MultiThread je nyní implementována pro platformu Windows. Aby bylo možné využít většinu hlavičkových souborů bez nutnosti je přepisovat nebo dopisovat do nich platformy, bývají interní údaje potřebné pro řízení vláken a daších operací skryta v neveřejných strukturách.

V třídě 'Thread' je vyhrazeno 16 bytů pro uložení privátních informací, jenž jsou uloženy v třídě 'ThreadBase_InfoStruct'. Pokud prostor 16 bajtů stačí, lze instanci této třídy alokovat přímo tam, pokud nestačí, musí si implementace alokovat prostor někde jinde, například na haldě a do vyhrazeného prostoru si uložit jen ukazatele.

16 bajtů vystačí na

  • ukazatel na virtuální tabulku (4 bajty)
  • 3 32-bitové proměnné identifikace vlákna.

nebo

  • 64-bitový ukazatel na vt
  • 64-bitový identifikátor vlákna nebo ukazatel na další data.

ve Win implementaci zde je

  • ukazatel na vt
  • handle threadu
  • id threadu

V třídě 'BlockerSimpleBase' která se používá pro synchronizační objekty přímo implementované operačním systémem je k dispozici 8 bajtů pro třídu 'BlockerInternal_InfoStruct'

8 bajtů vystačí na

  • dva 32-bit ukazatele případně na ukazatel na vt a nějaká dat (zpravidla ukazatel)
  • jeden 64-bit ukazatel.

ve Win implementaci zde je

  • handle objektu, zbytek je nevyužit.

Při přenosu na jinou platformu je potřeba reimplementovat všechny *.cpp soubory a soubory 'common_win.h' a 'waitFunctions_win.h'. Oba tyto soubory by se navíc neměly vkládat do ostatních částí programu. Pro uživatele knihovny MultiThread nejsou potřebné.

Výjimku zde tvoří vlastní implementace čekacích funkcích, pokud jsou přizpůsobené příslušnému OS (tedy nejedná se o implementaci pomocí kombinace již existujících čekacích funkcí - takové zdrojové kódy mohou vkládat oba soubory 'common_win.h' a 'waitFunctions_win.h'). Pak je nutné při přechodu na jinou platformu reimplementovat všechny tyto čekací funkce.

Pokročilá témata

Interlocked výpočty

V části Synchronizace byly představeny Interlocked funkce a bylo poukázáno na jejich velkou výhodu, totiž že v případě kolize vláken nezastavují vlákna, pouze na dobu operace zamknou sběrnici, čímž na pár taktu pozastaví činnost procesorů, jenž jsou v kolizi. Tohle je velice významné. Všechny ostatní synchronizační prostředky totiž vlákna, které nezískají přístup zastavují do okamžiku, než se prostředky, na které přistupují, uvolní. Slovo "do okamžiku" je velice nepřesné a je nutné to trochu upřesnit.

Bez ohledu na počet procesorů v počítači, čas jednotlivých procesorů je vláknům přidělován v kvantech. Jedno časové kvantum je z pohledu rychlosti procesoru mnoho času. Za tu dobu lze toho vykonat spoustu. Pokud však vlákno dospěje k nějakému synchronizačnímu prostředku a zjistí, že k němu nemá přístup, nesmí pokračovat, musí být zastaveno. Co to znamená?

  • Vlákno se musí pozastavit. Řízení přebírá OS
  • Vlákno se musí vzdát zbytku svého časového kvanta.
  • OS zpravidla ihned přidělí zbytek času jinému vláknu (což nemusí být pravidlem, například, pokud pro aktuální procesor není k dispozici jiné vlákno).
  • Pokud se nyní přístup synchronizačnímu prostředku uvolní, čekající vlákno se to dozví zpravidla až na začátku svého časového kvanta.

Představme si situaci, kdy např, kritická sekce uzamyká jedinou instrukci. Jakmile vlákno dorazi ke kritické sekci, a náhodou tato instrukce je zamčena, musí počkat, zpravidla však do následujícího přepnutí (jedine že by spuštěné vlákno muselo opět někde čekat a čekající vlákno by dostalo šanci dříve). Jedna instrukce tak netrvá několik taktů, ale několik miliónů taktů.

Pokud je to možné, používáme interlocked operace. K dispozici máme operace

  • Přičti jedničku (SyncInt::operator++)
  • Odečti jedničku (SyncInt::operator--)
  • Přičti číslo (SyncInt::operator+= a SyncInt::operator-=)
  • Vyměň obsah(Exchange)
  • Nastav obsah když obsahuje (SetValueWhen)

Třída SyncInt obsahuje i spoustu dalších operací, které jsou interlocked, a na nich si ukážeme, jak lze psát interlocked výpočty.

x=f(x,...)  

- jedná se o výpočet, kdy výsledek závisí kromě jiných proměnných, take na výsledku z předchozího výpočtu. Triviální operace např. příčtení jedničky má svou funkci. Ale co když potřebujeme např. přičíst jedničku a pokud je hodnota větší než jiná hodnota, nastavit -hodnotu?

x=(x+1);if (x>h) x=-h;

Složitější operace nelze prakticky napsat tak, aby během výpočtu nedošlo ke změně obsahu proměnné x. Při výpočtu je naštěstí jedno, zda ke změně došlo před začátkem výpočtu, nebo během něho, pokud vyžadujeme, aby výpočet byl atomický. Jakmile dojde k během výpočtu ke změně x, musí vlákno výpočet provést znovu.

int oldValue;
int newValue;
do
{
oldValue=x;
newValue=oldValue+1;
if (newValue>h) newValue=-h;
}
while (x!=oldValue);
x=newValue;

Tento příklad je správně ohledně myšlenky, ale ještě nebude fungovat. Pokud se x změní kdykoliv v bloku do-while, testem poznáme, že došlo ke změně a výpočet provedeme znovu. Problémem je, že ke změně může dojit po odtestování před uložením výsledku.

Třída SyncInt má na toto atomickou funkci "změn-obsah-když-obsahuje" ('SetValueWhen') Výsledkem operace je 'true' pokud funkce proběhla úspěšně, nebo 'false', pokud proměnná požadovanou hodnotu neobsahuje. Opravený příklad tedy vypadá takto:


int oldValue;
int newValue;
do
{
oldValue=x;
newValue=oldValue+1;
if (newValue>h) newValue=-h;
}
while (!x.SetValueWhen(oldValue,newValue));

Nyní je jasné, že když dojde ke změně kdykoliv během výpočtu, funkce 'SetValueWhen' to zjistí a proměnnou nezmění. Výsledek 'false' způsobí, že dojde k opakování výpočtu, nyní už s novou hodnotou. Samozřejmě že je možné, že i nyní dojde ke změně proměnné během výpočtu. Cyklus se tedy opakuje tak dlouho, dokud se vláknu nepodaří spočítat a "prosadit" svůj výsledek.

V praxi je však toto extrémní situace. Vezmeme-li v úvahu velikost časového kvanta a možný počet procesorů, tak, je-li blok s výpočtem v poměru doby časového kvanta krátký, ve většině případech proběhne cyklus jednou a jen občas dvakrát. Tři a více cyklů může nastat jen u víceprocesorových systémů. V porovnání s náročností čekání vlákna je to obrovský rozdíl.

POZOR: Nová verze:

MultiThread 2 <-- Stahujte zde!!!

Další díl

Multithreading v C++ (ve Win32) II - ThreadHook

vytvořeno: 1.6.2006 17:24:18, změněno: 30.3.2007 01:49:53
Jsou informace v článku pro Vás užitečné?
  • (11)
  • (0)
  • (1)
  • (2)
  • (1)
Nick nebo OpenID
Vzkaz
 
25.7.2016 17:42:12

CpMf1I1t

Hey, I just hopped over to your web page through StuUnlempob. Not somthing I would usually browse, but I liked your views none the less. Thanks for creating some thing worth browsing.
2.7.2014 10:44:50

riDPy6FK4

No ja teda naopak musim ředct že 1. Boss mě zlakmal. Ono jako všeobecně když tam do nějake9ho hre1če pe1led ze všech stran a jemu to de1ve1 směšnej dmg za 10, 25, 15 a na ople1tku npc-čko dostane hit za 500 Kdyby ty re1ny takhle uste1l tank tak neřeknu ale bez ohledu na to kdo zrovna doste1va dmg tak je to nic moc hrozba teda Prvnedch 5 min vypade1 jako když hight lvly vlezou do operace určene9 pro low lvl Tak doufe1m že je to opravdu jen tedm že je to ofiko video
16.3.2010 08:23:47

nick

"><script>alert("test");</script>
13.3.2009 22:09:27

Ondra openid

Hmm, to je divný. Chybové hlášení říká, že hlavní vlákno... to které se spouští v Main musí mít taky associovaný objekt. Mělo by stačit v main vytvořit proměnnou RunningThread<ThreadBase> mainThread; což attachne ten objekt k hlavnímu vláknu.

Jinak se zkuste se podívat, proč ten assert na te radce 51 vypadl.

Uvědomuju si, že knihovna je skoro tři roky stará a za dobu, co ji používají v BIStudiu se objevili nějaké problémy, které se opravily. Protože ale nebyl na tuto knihovnu repozitář, není kam uploadovat změny. V současné době mám ve vývoji novou knihovnu součástí knihovny LightSpeed. "lightspeed/mt". Vývojovou verzi knihovny lze stáhnout na adrese:

(svn) https://light-speed.svn.sourceforge.net/svnroot/light-speed/branches/devel/lightspeed/

(složka mt/, ale je závislá na složce base/)

pozor, bez záruky, ve vývoji, manuál je jen ve formě komentářů, zato k tomuto mohu poskytnout lepší support a pružněji reagovat na chyby, protože prakticky deně tenhle repozitář aktualizuju
13.3.2009 11:52:05

Dawe

Ahoj, mám problém s rozchozením knihovny MultiThread 2.

Pro kód:

class MyThread1: public ThreadBase
{
unsigned long Run() {
//nikdy se nevykona
cout << "1256465" << endl;
}
};

int main(int argc, char *argv[]) {

cout << "Start" << endl;

MyThread1 myThread1;
myThread1.Start();

cout << "End" << endl;

return EXIT_SUCCESS;
}

Dostanu vždy následující výstup :(
Start
... Thread Object created, 094304A8
End
0ASD
+----------------------------------------------------------------------------------------------+
ASSERT FAILED:
multithread/src-linux/common_linux.cpp:51: Cannot handle operation - this thread is not controlled by MultiThread library
Expression: thread!=0

(A)bort, (B)reak, (C)ontinue, (I)gnore Always, or (Enter) to continue

+----------------------------------------------------------------------------------------------+
Thread id B7F40BA0, object BFF0CAB0, has been started.
pure virtual method called
terminate called without an active exception
Aborted
12.1.2007 23:10:24

--==[FReeZ]==--

Velmi pekne stranky, hned si jdu kliknout na RSS =)

Podobné články

Multithreading v C++ (ve Win32) II - ThreadHook

Pokračování dokumentace k Multithreading v C++ (ve Win32)

Multithreading v C++ (ve Win32) III - OOP a vlákna

Pokračování dokumentace k Multithreading v C++ (ve Win32) - Tentokrát si povíme něco o tom jak začlenit vlákna do OOP

MultiThread 2

Knihovna MultiThread-2.0

Základy komunikace mezi procesy (ve Windows)

Od počátku Windows95 jsou procesy v paměti oddělené. Každý proces je vlastně svým světem sám pro sebe. Jak tedy mohou komunikovat procesy mezi sebou? Podrobný výčet možností...
Reklama: