- předchozí článek - následující článek - obsah - úvodní stránka -

Linuxové noviny 08-09/98

Linux a vlákna

Vladimír Michl, 7. srpna 1998

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.

Příklad, jak funkce použít, naleznete na výpise Příklad použití vláken.


#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;
}

Výpis č. 1: Příklad použití vláken

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 na výpise Příklad použití vláken 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);

Výpis č. 2: Schematické znázornění použití mutexu

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;
}

Výpis č. 3: Použití mutexu v problému producent - konzument

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);

  • slouží 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;
}

Výpis č. 4: Použití semaforů u problému producent - konzument

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é.

Jak použít funkce můžete vidět na příkladu Ukončení vlákna.


#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;
}

Výpis č. 5: Ukončení vlákna

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í pthread_join(3).

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++. *


- předchozí článek - následující článek - obsah - úvodní stránka -