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.
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ř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
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);
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);
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);
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
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);
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.
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
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ů.
- 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.
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++.
|