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ň