Linux a vlákna
                                 -=-=-=-=-=-=-=-=

          Tento článek si klade za úkol seznámit čtenáře s vlákny v Linuxu
      a jejich  použitím,  případně s tím,  čeho  by  se  měl  člověk  při
      používání vláken vyvarovat.

          Ale od začátku.  Nejprve je třeba osvětlit  rozdíl mezi termínem
      proces a vlákno.

          Jako  proces  je v  systému   chápán   souhrn   kódu   programu,
      dat  programu,  zásobníku,  údajů o procesem  otevřených  souborech,
      a také informací ohledně zpracování  signálů. Tyto všechny informace
      má  každý  proces  vlastní  (privátní)  a nemůže  je  sdílet s jiným
      procesem, kromě  datových  oblastí. Při volání jádra  fork(2) se pak
      tyto informace pro nový  proces  skopírují,  takže jsou pro něj zase
      privátní.

          Jako vlákno si můžeme představit odlehčený proces, tj. pouze kód
      vlákna a zásobník, vše ostatní je sdíleno s ostatními  vlákny  téhož
      procesu.  Vlákno je tedy podmnožinou  procesu a proces může vlastnit
      několik vláken. Vlákno samo o sobě v systému  existovat nemůže, musí
      k němu vždy existovat proces, se kterým sdílí všechna data, otevřené
      soubory, zpracování signálů.

          Pro implementaci vláken existují tyto modely:

          one-to-one  - Implementace  provedena  na  úrovni  jádra.  Každé
      vlákno je pro  jádro  samostatný  proces,  plánovač  procesů  nečiní
      rozdíl mezi  vláknem a procesem.  Nevýhodou  tohoto  modelu může být
      velká režie při přepínání vláken.

          many-to-one - Implementace   provedena   na  úrovni   uživatele,
      program  si  sám    implementuje    vlákna  a  vše   okolo.    Jádro
      o vláknech v procesech  nemá  ani  tušení.  Tento  model  se  nehodí
      na víceprocesorové  systémy,  protože  vlákna  nemohou běžet zároveň
      (každé na jiném  procesoru),  jeden  proces nelze  nechat  vykonávat
      na dvou procesorech. Výhodou může být malá režie přepínání vláken.

          many-to-many  -  Implementace    provedena   na  úrovni    jádra
      i uživatele. Tento model eliminuje nevýhody předchozích implementací
      (velká  režie při přepínání  procesů,  souběžně  nemůže  běžet  více
      vláken)  a je  proto  použit v mnoha  komerčních  UNIXech  (Solaris,
      Digital Unix, IRIX).

          V Linuxu je použit model první.  Nevýhoda velké režie v podstatě
      není,  protože  přepínání  procesů je v Linuxu  implementováno velmi
      efektivně. Pro tvorbu  procesů a vláken se v Linuxu  používá  volání
      jádra clone(2),  které ale používají pouze knihovny  obhospodařující
      vlákna.

          V začátcích, kdy se v Unixech začala vlákna objevovat, měl každý
      unixový operační systém jiné aplikační  rozhraní pro práci s vlákny,
      a proto byly  programy  špatně  přenositelné.  Proto  vznikla  norma
      POSIX,  která mimo jiné také definuje  aplikační  rozhraní pro práci
      s vlákny (POSIX 1003.1c). Toto POSIXové rozhraní je dostupné i na OS
      Solaris 2.5,  Digital Unix 4.0, IRIX 6. S každou  distribucí  Linuxu
      postavenou na glibc-2.0 je dodávána  knihovna  pthread,  která právě
      toto POSIXové aplikační rozhraní implementuje.

                        Tvorba vláken a jejich ukončení

          Pro vytvoření a ukončení vlákna lze použít následující funkce:

      int  pthread_create(pthread_t * thread,
       pthread_attr_t * attr, void * (*start_routine)(void *), void * arg);

          funkce   vytvoří  nové  vlákno,  které  bude  vykonávat   funkci
      start_routine, což je funkce  akceptující jeden parametr typu void*.
      Na  adresu  thread je uložen  identifikátor  vlákna a jako  atributy
      vlákna můžeme uvést NULL pro implicitní hodnoty.

      void pthread_exit(void *retval);

          tato funkce  předčasně  ukončí  vlákno, ze kterého  byla  funkce
      zavolána.

          Vlákno se také ukončí,  skončí-li  funkce  start_routine. V obou
      případech se předává návratový kód.

      int pthread_join(pthread_t th, void **thread_return);

          funkce  čeká na ukončení  vlákna th. Na adresu  thread_return je
      uložen návratový kód vlákna.

#include <pthread.h>
#include <stdio.h>
#define ITEMS 10000

void * process(void *a){
      int i;
      printf("Process %s: start\n", (char *)a);
      for (i = 0; i<ITEMS; i++){
            printf("%s", (char *)a);
      };
      printf("Process %s: end\n", (char *)a);
      return NULL;
}

int main(){
      int retcode;
      pthread_t a,b;
      void * retval;

      retcode = pthread_create(&a, NULL, process, "A");
      if (retcode != 0) fprintf(stderr, "create a failed %d\n", retcode);
      retcode = pthread_create(&b, NULL, process, "B");
      if (retcode != 0) fprintf(stderr, "create b failed %d\n", retcode);
      retcode = pthread_join(a, &retval);
      if (retcode != 0) fprintf(stderr, "join a failed %d\n", retcode);
      retcode = pthread_join(b, &retval);
      if (retcode != 0) fprintf(stderr, "join b failed %d\n", retcode);
      return 0;
}


                                 Překlad programu

          Při překládání  programu, který používá  vlákna, je třeba tomuto
      přizpůsobit   hlavičkové   soubory  knihovny  glibc  tak,  aby  byly
      reentrantní. To  provedeme  definováním  makra  _REENTRANT.  Dále je
      třeba program  slinkovat s knihovnou  pthread. Pro překlad  programu
      použijeme:

      gcc -D_REENTRANT -o example1 example1.c -lpthread

                      Kritické sekce pomocí mutexu

          Nejprve si položme otázku, co je to kritická sekce. Za kritickou
      sekci  považujeme tu část kódu vlákna,  která  operuje nad sdílenými
      daty a hrozí, že paralelně  může jiné  vlákno  operovat nad stejnými
      daty.  Důsledkem může být nekonzistence dat. Například  jedno vlákno
      zvýší  sdílenou  proměnnou  A o  jedna  a dále s ní  počítá,  kdežto
      druhé  vlákno  proměnou A zmenší o dvě a dále s ní počítá.  Pokud se
      poštěstí, tak se instrukce mohou  proložit tak, že ani jedno  vlákno
      nedá správný  výsledek.  Tomuto je třeba zabránit a to tím, že do té
      části, která pracuje s proměnnou A může vstoupit pouze jedno vlákno,
      druhé  musí  čekat  až to první  skončí.  Takovéto  kritické  sekce,
      kde může být v jednom  okamžiku pouze jedno  vlákno,  nazýváme MUTEX
      (MUTual  EXclusion). Mutex má dva stavy - zamčený  (locked - některé
      vlákno je uvnitř) a odemčený (unlocked - v mutexu nikdo není).

          Pro práci s mutexy použijeme funkce:

       int pthread_mutex_init(pthread_mutex_t *mutex,
              const  pthread_mutexattr_t *mutexattr);

          inicializace mutexu

       int pthread_mutex_lock(pthread_mutex_t *mutex);

          zamčení  mutexu. Po návratu je mutex  vždy  zamčen  pro  vlákno,
      které  tuto  funkci  vykonalo.  Pokud je mutex  již  zamčen,  funkce
      pozastaví  vlákno a čeká na  odemčení  mutexu,  aby  následně  mutex
      zamkla a mohla nechat vlákno pokračovat.

      int pthread_mutex_trylock(pthread_mutex_t *mutex);

          pokus o zamčení  mutexu.  Pokud je mutex již  zamčen,  funkce se
      vrátí s chybou EBUSY.

      int pthread_mutex_unlock(pthread_mutex_t *mutex);

          odemčení mutexu

      int pthread_mutex_destroy(pthread_mutex_t *mutex);

          uvolnění zdrojů spojených s mutexem

          V příkladu  Schematické  znázornění  použití mutexu můžete vidět
      použití mutexu.

      pthread_mutex_t mut_var;
      ...
      /* Inicializace mutexu */
            pthread_mutex_init(&mut_var, NULL);
      ...
      /* Vstup do mutexu */
            pthread_mutex_lock(&mut_var);
      /* Vykonání operací nad sdílenými daty */
      ...
      /* Výstup z mutexu */
            pthread_mutex_unlock(&mut_var);
      ...
      /* Na konci programu zrušení mutexu */
            pthread_mutex_destroy(&mut_var);

          U mutexů se můžeme  setkat s tím, že bude třeba  mutex  zamknout
      v závislosti na podmínce.  Například problém  producent - konzument.
      Producent  produkuje  data do sdílené  proměnné a konzument  je čte.
      Přitom  proměnná  musí  být  zabezpečena  mutexem a zároveň  se musí
      hlídat  stav, zda proměnná  obsahuje  užitečná data. I na toto POSIX
      myslí, a to pomocí následujících funkcí:

      int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)

          inicializace podmínky

      int pthread_cond_signal(pthread_cond_t *cond);

          způsobí  spuštění  jednoho   vlákna,  které  čeká  na  podmínce.
      Jestliže  nečeká žádné vlákno, funkce nemá žádný efekt. Čeká-li více
      vláken, spustí se pouze jedno, ale není definováno jaké.

      int pthread_cond_broadcast(pthread_cond_t *cond);

          způsobí  spuštění všech vláken čekajících na podmínce.  Jestliže
      nečeká žádné vlákno, funkce nemá žádný efekt.

      int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

          automaticky  odemkne  mutex,  pozastaví  vlákno a čeká na signál
      od podmínky.  Po  příchodu  signálu je mutex  uzamčen a tato  funkce
      ukončena. Každá podmínka musí být uzavřena v mutexu.

      int pthread_cond_timedwait(pthread_cond_t *cond,
             pthread_mutex_t *mutex,
             const struct timespec *abstime);

          je podobné  pthread_cond_wait() s tím  rozdílem,  že  čekání  je
      časově  omezeno. Pokud čas vyprší, pak je sekce uzamčena a funkce je
      ukončena s chybou ETIMEDOUT.

      int pthread_cond_destroy(pthread_cond_t *cond);

          uvolní zdroje spojené s podmínkou

          V  příkladu  Použití  mutexu v problému  producent  -  konzument
      můžete  vidět  použití  mutexu a podmínek  na  problému  producent -
      konzument. Všimněte si rozdílné  inicializace  podmínek,  samozřejmě
      obě podmínky jdou inicializovat stejným způsobem.

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define ITEMS 100
#define NONVALID 0
#define VALID 1
pthread_mutex_t mut_var;
pthread_cond_t condvalid;
pthread_cond_t condnonvalid = PTHREAD_COND_INITIALIZER;
int valid;
int share;

void * konzument(void *a){
      printf("Process %s: start\n", (char *)a);
      while(1){
            pthread_mutex_lock(&mut_var);
            if (!valid)
              pthread_cond_wait(&condvalid, &mut_var);
            valid = NONVALID;
            printf("Process %s: %i\n", (char *)a, share);
            if (share == -1){
                  pthread_mutex_unlock(&mut_var);
                  break;
            };
            pthread_cond_signal(&condnonvalid);
            pthread_mutex_unlock(&mut_var);
      };
      printf("Process %s: end\n", (char *)a);
      return NULL;
}

void * producent(void *a){
      int i;
      printf("Process %s: start\n", (char *)a);
      for (i = 0; i<ITEMS; i++){
            pthread_mutex_lock(&mut_var);
            if (valid)
              pthread_cond_wait(&condnonvalid, &mut_var);
            share = (int)rand();
            if (share == -1) share = 0;
            if (i == ITEMS - 1) share = -1;
            printf("Process %s: %i\n", (char *)a, share);
            valid = VALID;
            pthread_cond_signal(&condvalid);
            pthread_mutex_unlock(&mut_var);
      };
      printf("Process %s: end\n", (char *)a);
      return NULL;
}

int main(){
      pthread_t a,b;
      pthread_mutex_init(&mut_var, NULL);
      pthread_cond_init(&condvalid, NULL);
      pthread_create(&a, NULL, producent, "producent");
      pthread_create(&b, NULL, konzument, "konzument");
      pthread_join(a, NULL);
      pthread_join(b, NULL);
      pthread_cond_destroy(&condvalid);
      pthread_cond_destroy(&condnonvalid);
      pthread_mutex_destroy(&mut_var);
      return 0;
}


                       Kritické sekce pomocí semaforů

          Semafory se používají  pro podobný  účel  jako  mutexy, a to pro
      kontrolování  vstupu do kritických  sekcí.  Ale na rozdíl od mutexu,
      kdy  v  sekci  může  být  pouze  jeden,  se  semafory  lze  docílit,
      že v sekci může být více vláken.  Semafor si můžeme  představit jako
      počítadlo s počáteční  hodnotou, kterou  nastaví  uživatel. Vždy při
      vstupu do kritické sekce se čeká, dokud není hodnota  semaforu větší
      než  nula.  Pokud  je,  pak se  hodnota  zmenší o jednu a vstoupí se
      do  kritické  sekce. Na konci  sekce se hodnota  semaforu o jedničku
      zvedne. Pro práci se semafory používáme funkce:

      int sem_init(sem_t *sem, int pshared, unsigned int value);

          inicializace  semaforu.  Argument pshared určuje, zda je semafor
      lokální pro tento  proces  (hodnota 0) nebo je sdílen  mezi  procesy
      (hodnota != 0). V Linuxu jsou podporovány pouze lokální semafory.

      int sem_wait(sem_t * sem);

          slouží  pro  vstup do kritické  sekce.  Pokud je sekce  obsazena
      (semafor == 0), pak se čeká až se sekce uvolní.

      int sem_trywait(sem_t * sem);

          louží pro vstup do kritické sekce. Je-li sekce obsazena,  funkce
      se vrátí s chybou EAGAIN.

      int sem_post(sem_t * sem);

          slouží k ukončení kritické sekce.

      int sem_getvalue(sem_t * sem, int * sval);

          vrátí hodnotu semaforu.

      int sem_destroy(sem_t * sem);

          uvolní všechny zdroje spojené se semaforem.


#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#define ITEMS 100

sem_t semfull;
sem_t semempty;
int share;

void * konzument(void *a){
  printf("Process %s: start\n", (char *)a);
  while(1){
    sem_wait(&semfull);
    printf("Process %s: %i\n", (char *)a, share);
    if (share == -1){
      sem_post(&semempty);
      break;
    };
    sem_post(&semempty);
  };
  printf("Process %s: end\n", (char *)a);
  return NULL;
}

void * producent(void *a){
  int i;
  printf("Process %s: start\n", (char *)a);
  for (i = 0; i<ITEMS; i++){
    sem_wait(&semempty);
    share = (int)rand();
    if (share == -1) share = 0;
    if (i == ITEMS - 1) share = -1;
    printf("Process %s: %i\n", (char *)a, share);
    sem_post(&semfull);
  };
  printf("Process %s: end\n", (char *)a);
  return NULL;
}

int main(){
      pthread_t a,b;

      sem_init(&semfull, 0, 0);
      sem_init(&semempty, 0, 1);
      pthread_create(&a, NULL, producent,
          "producent");
      pthread_create(&b, NULL, konzument,
          "konzument");
      pthread_join(a, NULL);
      pthread_join(b, NULL);
      sem_destroy(&semfull);
      sem_destroy(&semempty);
      return 0;
}


          Toto rozhraní pro semafory  definuje norma POSIX 1003.1b a POSIX
      1003.1i.

                           Ukončení vlákna jiným vláknem

      int pthread_cancel(pthread_t thread);

          vyvolá požadavek na zrušení vlákna.

      int pthread_setcancelstate(int state, int *oldstate);

          nastaví chování vlákna, které tuto funkci vyvolalo, na požadavek
      jeho   zrušení.   Možné   jsou  dva   stavy:   PTHREAD_CANCEL_ENABLE
      a PTHREAD_CANCEL_DISABLE.

      int pthread_setcanceltype(int type, int *oldtype);

          nastaví,   kdy  je  možno   vlákno   zrušit.   Možné   jsou  dvě
      nastavení:  PTHREAD_CANCEL_ASYNCHRONOUS - vlákno bude zrušeno  skoro
      okamžitě  po  přijetí   požadavku   nebo   PTHREAD_CANCEL_DEFERRED -
      vlákno  se  zruší až v okamžiku,  kdy  dojde do bodu,  kde je  možno
      vlákno  zrušit.  Jako  body jsou v POSIXu  definovány  tyto  funkce:
      pthread_join(3),   pthread_cond_wait(3),  pthread_cond_timedwait(3),
      pthread_testcancel(3), sem_wait(3), sigwait(3).

      void pthread_testcancel(void);

          tato funkce pouze  testuje, zda byl přijat  požadavek na zrušení
      vlákna.  Pokud  přijat byl, vlákno je zrušeno, v opačném  případě se
      funkce normálně  vrátí. Funkce se používá v místech, kde jsou dlouhé
      kusy kódu bez bodů vhodných pro zrušení.

          Pokud   je   vlákno   v   bodu   vhodném   pro   zrušení    (viz
      pthread_setcanceltype(3))  a  přijalo  požadavek  na  zrušení,  bude
      zrušeno.  Totéž se stane,  pokud  přijalo  požadavek na zrušení a až
      následně  vejde do bodu  vhodného pro zrušení  (pouze při nastaveném
      PTHREAD_CANCEL_DEFERRED).

          Pokud se ukončí  hlavní  vlákno,  aniž by počkalo na vlákna  jím
      vytvořená, jsou tato vlákna ukončena také.


#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>

sem_t sem;

void * process(void *a){
   printf("Proces entered\n");
   sem_wait(&sem);
   printf("Proces exiting\n");
   return NULL;
}

int main(){
   pthread_t thid;
   void * thretval;

   sem_init(&sem, 0, 0);

   pthread_create(&thid, NULL, process, NULL);

   sleep(5); /* Vlákno se zatím rozběhne */
   if (pthread_cancel(thid) != 0)
     printf("Cancel error\n");

   pthread_join(thid[i], &thretval);
   if (thretval != PTHREAD_CANCELED)
     printf("Thread not be canceled.\n", i);

   return 0;
}


                                  Další užitečné funkce

     pthread_t pthread_self(void);

          vrací identifikátor vlákna, které tuto funkci vyvolalo.

     int pthread_equal(pthread_t thread1, pthread_t thread2);

          porovná, zda se identifikátory vláken rovnají.

     int pthread_detach(pthread_t th);

          odpojí  vlákno.  Všechny   paměťové   prostředky,  které  vlákno
      používá,  budou po ukončení  vlákna  okamžitě  uvolněny. S odpojeným
      vláknem se  nelze  synchronizovat a vyzvednout  jeho  návratový  kód
      funkcí <TT>pthread_join(3)</TT>.

     int pthread_attr_init(pthread_attr_t *attr);

          inicializuje objekt atributů na implicitní hodnoty. Tento objekt
      se dá použít pro vytvoření více vláken.

     int pthread_attr_destroy(pthread_attr_t *attr);

          uvolní všechny prostředky potřebné pro objekt atributů.

                       Problémy, do kterých se můžete dostat

          knihovna  pro  vlákna   používá   signály   SIGUSR1  a  SIGUSR2,
      proto je program  používat  nemůže. pokud budete vlákna používat v X
      aplikacích, je třeba mít Xlib  kompilovánu s -D_REENTRANT a podporou
      vláken  (knihovna  musí být napsána  reentrantně - více vláken  může
      vykonávat tutéž funkci ve stejnou  chvíli, bez vzájemného  ovlivnění
      - funkce  nepoužívají  globální  proměnné). Totéž  platí o jakékoliv
      knihovně,  kterou  budete v programu  používat. Pokud knihovna takto
      reentrantní není, je možno ji používat, ale pouze z hlavního  vlákna
      (kódu  procesu).  Toto souvisí s proměnnou  errno.  Každé  vlákno má
      totiž vlastní, pouze hlavní vlákno používá globální errno.

          používání  vláken  v  C++ s  libg++  asi  nebude  fungovat.  Pro
      používání  vláken  v C++  je  doporučen  překladač  egcs a  knihovna
      libstdc++.

          pokud program vytvoří například 2 vlákna,  nedivte se, že vidíte
      4 stejné  procesy.  Jeden  je  hlavní  proces,  pak  vidíte 2 vlákna
      a poslední je vlákno starající se o správný chod vláken. Toto vlákno
      je vytvořeno knihovnou pthread.

                                      Odkazy

          Na adrese  http://www.serpentine.com/bos/threads-faq/ lze najít
      často kladené otázky newsové skupiny comp.programming.threads

          Bližší  informace o  linuxových  vláknech  naleznete  na  adrese
      http://pauillac.inria.fr/xleroy/linuxthreads.  Lze zde  také  najít
      tutorial.

          Na                                                        adrese
      http://www.rdg.opengroup.org/onlinepubs/7908799/index.html   najdete
      X/Open Group Single Unix specification, kde by se měl také dát najít
      bližší popis aplikačního rozhraní pro vlákna.

          Na  adrese   http://www.cs.wustl.edu/schmidt/ACE.html   najdete
      projekt, který také usnadňuje používání vláken v C++.


            výheň