9 - Vícevláknové aplikace

  • Vlákna (pthread)
  • Kritická sekce a zámky (mutex)
  • Synchronizace vláken - podmíněná proměnná (Conditional Variable)
  • Pro vyučující: 09

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í stiknuté 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 lab09.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 <stdbool.h> */
      while (!q) {
         usleep(100 * 1000);  /* from the <unistd.h> */
         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
  • 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 hw09.

courses/b3b36prg/labs/lab09.txt · Last modified: 2018/02/06 17:18 (external edit)