{{indexmenu_n>10}}
====== 10 - Vícevláknové aplikace ======
* Pro vyučující: [[courses:b3b36prg:internal:tutorialinstruction:10|]]
==== Procvičovaná témata ====
* Vlákna (pthread)
* Kritická sekce a zámky (mutex)
* Synchronizace vláken - podmíněná proměnná (Conditional Variable)
^ Výchozí soubory | {{:courses:b3b36prg:labs:prg-lab10.zip|}}|
Cílem cvičení je implementovat program s třemi paralelně běžícími vlákny. První vlákno je určeno pro zpracování vstupu (čtení stisknuté klávesy), druhé vlákno aktualizuje výstup (jednořádkový) a třetí vlákno implementuje časovač, který po uplynutí definované periody zvýší hodnotu proměnné (čítače). Perioda může být nastavována uživatelem, stiskem definovaných kláves. Použití vláken je založeno na knihovně ''pthread'' (POSIX threads) a příklad demonstruje použití základních konstrukcí ''pthread'' knihovny.
===== První vlákna =====
* Využijte výchozí zdrojové soubory {{:courses:b3b36prg:labs:prg-lab10.zip|}}
* Vytvořte dvě funkce zabalující výpočetní tok jednotlivých vláken
void* thread1(void*);
void* thread2(void*);
* V ''thread1()'' implementujte cyklus, který inkrementuje global proměnnou ''counter'' s periodou 100 ms, např.,
void* thread1(void *v)
{
bool q = false; /* from the */
while (!q) {
usleep(100 * 1000); /* from the */
counter += 1;
}
return 0;
}
* Ve funkci ''thread2()'' implementujte aktualizaci jednořádkového výstupu s aktuální hodnotou proměnné ''counter'', např.
void* thread2(void *v)
{
bool q = false;
while (!q) {
printf(*"\rCounter %10i", counter);
}
return 0;
}
* Vytvořte vlákna voláním funkce ''pthread_create()'' s předáním ukazatele na příslušnou funkci ''thread1()'' a ''thread2()'' např.
counter = 0;
pthread_t thrs[2];
pthread_create(&thrs[0], NULL, thread1, NULL);
pthread_create(&thrs[1], NULL, thread2, NULL);
Jakmile je vlákno vytvořeno, je spuštěno a začne vykonávat tělo předané funkce.
* Přidejte do programu čekání na stisk klávesy ''Enter'' a následně vyčkejte ukončení všech vláken voláním ''pthread_join()'', např.
getchar();
for (int i = 0; i < 2; ++i) {
pthread_join(thrs[i], NULL);
}
===== Využití zámku (mutex) pro definování kritické sekce =====
V případě, kdy je nutné přistupovat k proměnné sdílené více vlákny je zpravidla žádoucí zabranit souběhu definováním kritické sekce zámkem (mutexem), který dovolí vstup do sekce pouze jedinému vláknu. Kritická sekce může být např. implementována s využitím zámku ''pthread_mutex_t''.
* Vytvořte globální proměnnou ''mtx'', kterou inicializujte funkcí ''pthread_mutex_init()'' s výchozími atributy, např.
pthread_mutex_t mtx;
pthread_mutex_init(&mtx, NULL);
* Inicializaci mutexu proveďte před prvním použitím v příslušném vlákně. Myslete na to, že vlákno může být spuštěno dříve, než očekáváte. V podstatě spíše počítejte s tím, že vlákno je spuštěno ihned po vytvoření.
* Přístup ke globální proměnné ''counter'' dejte do kritické sekce použitím ''pthead_mutex_lock()'' a ''pthread_mutex_unlock()'', např.
void *thread1(void *v)
{
...
pthread_mutex_lock(&mtx);
counter += 1;
pthread_mutex_unlock(&mtx);
...
}
* Vytvořte novou globální proměnnou ''quit'' k indikaci, že program bude ukončen jakmile uživatel stiskne klávesu ''Enter'' např.
bool quit = false;
...
gechar();
pthread_mutex_lock(&mtx);
quit = true;
pthread_mutex_unlock(&mtx);
...
* Proměnnou ''quit'' použijte pro ukončení smyčky ve funkci ''thread1()'' i ''thread2()'', např.
void* thread1(void *v)
{
...
while (!q) {
...
pthread_mutex_lock(&mtx);
...
q = quit;
....
===== Použití podmíněné proměnné (Conditional Variable) pro synchronizaci vláken =====
Aktuální implementace aktualizace výpisu v cyklu ''thread2'' je výpočetně velmi náročná, neboť cyklus je opakován tak rychle jak dovolují prostředky CPU. V našem případě je taková extermní frekvence aktualizace zbytečná a neefektivní (např. při běhu notebooku/tabletu na baterii se zbytečně čerpá omezená energie a navíc zařízení zahříváme). V podstatě nám stačí aktualizovat výpis pouze v případě, kdy dojde ke změně hodnoty proměnné ''counter'' Takovou synchronizaci vláken můžeme implementovat mechanismem podmíněných proměnných (conditional variable), tj. proměnná typu ''pthread_cond_t'', kterou použijeme k pozastavení vykonávání vlákna voláním ''pthread_cond_wait()''. Takto čekající vlákno můžeme následně probudit voláním ''pthread_cond_singnal()'' nebo ''pthread_cond_broadcast()''.
* Deklarujte globální proměnnou ''condvar'' pro podmíněnou proměnnou a inicializujte ji výchozími parametry, např.
pthread_cond_t condvar;
...
pthread_cond_init(&condvar, NULL);
* Vykonávání vlákna může být pozastaveno voláním funkce ''pthread_cond_wait()'', která uvolní předaný (zamčený) zámek (mutex) a vyčká na signál podmíněné proměnné. Jakmile je signál přijat, je zahájeno znovu získání zámku (tj. pokud je zamčen, vlákno je pozastaveno a čeká na uvolnění tak jako při vstupu do kritické sekce) a následně pokračuje ve vykonávání. Tělo vlákna ''thread2()'' aktualizujte pro použití podmíněné proměnné ''condvar'' a zámku ''mtx''.
void* thread2(void *d)
{
...
pthread_mutex_lock(&mtx);
...
pthread_cond_wait(&condvar, &mtx);
...
pthread_mutex_unlock(&mtx);
...
* Vláknu ''thread2()'' pošleme signál voláním ''pthread_cond_signal()'' vždy když dojde k zvyčení hodnoty čítače ''counter''
void* thread1(void *d)
{
...
pthread_mutex_lock(&mtx);
counter += 1;
...
pthread_cond_signal(&condvar);
...
pthread_mutex_unlock(&mtx);
...
* Jelikož výstupní řádek není zakončen koncem řádku může být nutné explicitně vynutit výstup voláním ''flush'' na ''stdout'' ve výstupním vlákně, např. jako fflush(stdout);
===== Úkoly =====
* Použijte výše uvedené bloky pro vytvoření aplikace se třemi vlákny. Ve skutečnosti bude mít aplikace vlákna 4, včetně hlavního vlákna ''main()'' funkce
* Pro čtení kláves bez nutnosti potvrzovat ''Enter'' přepněte terminál do ''raw'' režimu, například tak jak bylo prezentována v přednášce lec08
void set_raw(int set)
{
static struct termios tio, tioOld;
tcgetattr(STDIN_FILENO, &tio);
if (set) {
tioOld = tio; //backup
cfmakeraw(&tio);
tio.c_lflag &= ~ECHO; // assure echo is disabled
tio.c_oflag |= OPOST; // enable output postprocessing
tcsetattr(STDIN_FILENO, TCSANOW, &tio);
} else {
tcsetattr(STDIN_FILENO, TCSANOW, &tioOld);
}
}
* Použijte strukturu pro zapouzdření proměnných sdílených jednotlivými vlákny
* Použijte kritickou sekci (mutex) pro přístup ke sdíleným datům
* Použijte zasílání signálů pro komunikaci mezi vlákny a zabránění plýtvání výpočetním výkonem
* Přidejte jméno (textový řetězec) identifikující každé vlákno: "Input", "Output", and "Alarm"
* "Alarm" vlákno inkrementálně zvyšuje hodnotu čítače po uplynutí definové periody
* Použijte pole a datové struktury, které umožní zautomatizovat správu vláken v cyklech
* Program bude reagovat na následující stisk kláves
* 'q' - ukončí program tak, že všechna vlákna budou korektně ukončena, tj. vyskočí z příslušných cyklech hlavního těla vlákna
* 'r' - sníží periodu pro inkrementaci čítače o 10 ms
* 'p' - zvýší periodu pro inkrementaci čítače o 10 ms
* Minimální hodnota periody je 10 ms
* Maximální hodnota periody je 2000 ms
* Výstup programu je jeden řádek ve tvaru "\rAlarm period: %10i Alarm counter: %10i"
===== Další úkoly na cvičení =====
V další části cvičení pokračujte implementací domácího úkolu [[courses:b3b36prg:hw:hw09|]].