Obr. 1. Adresové prostory nezávislých procesů a vláken v jednom procesu
Podpora vláken v jádru systému je nezbytná pro víceprocesorové systémy, protože přidělování procesorů na úrovni procesů nedovoluje jednomu procesu využít více procesorů než jednoho. Podpora vláken na úrovni uživatelského procesu byla použita pro implementaci vláken v době, kdy nebyla podpora vláken zahrnuta do běžně dostupných operačních systémů (např. OSF DCE [3]). Vytváří v jednom procesu více paralelně běžících vláken. Více vláken v uživatelském procesu je tedy zobrazeno na jeden proces z hlediska jádra systému - zobrazení N:1.
Pokud je podpora vláken zabudována do jádra operačního systému, je procesor přidělován ne na úrovni procesů, ale na úrovni vláken. Každé vlákno je jakoby jedním procesem z klasických operačních systémů. Při volání služeb jádra systému, které pozastavují proces, jako je například read(), není pozastaven proces, ale pouze prováděné vlákno. Termín proces v těchto systémech zahrnuje několik paralelně běžících vláken. Klasický proces odpovídá procesu s jedním aktivním vláknem. Identita procesu, majitelství, ochrana, spouštění procesů zůstává zachována, ale vše je interpretováno ve smyslu "identita procesu, jehož součástí je vlákno, které je právě prováděno". Pokud je každé vlákno uvnitř procesu identifikováno a obhospodařováno jádrem systému, jedná se o zobrazení uživatelských vláken na systémové N:N [4].
Při větším počtu systémových vláken vytvořených uvnitř jednoho procesu stoupá režie spojená se systémovou identifikací vláken. Přitom počet fyzicky souběžně prováděných vláken nemůže být větší než je počet procesorů. Pokud většina vláken čeká na synchronizaci, jsou datové struktury pro systémovou identifikaci vláken v jádru z větší části mrtvou režií a navíc je nelze odložit na disk. Proto některé implementace vláken dovolují vytvořit zobrazení vláken uživatelských na systémová typu N:M, kde N > M. Vlákna na úrovni uživatelské jsou obhospodařována knihovnami, které umožňují vytvářet, spouštět a synchronizovat vlákna. Uživatelská vlákna jsou knihovnou přiřazena na vlákna jádra systému a přiřazení může být od typu N:1, až po N:N. Vlákna na úrovni jádra systému jsou pak nazývána Light Weight Process. Přiřazení uživatelských vláken na systémová může být buď volné (unbound), anebo pevné (bound) [5].
Naskýtá se otázka, jak volit přiřazení uživatelských vláken na systémová. Volné přiřazení je systémově nezávislé, systém může přiřadit systémová vlákna podle počtu procesorů. Pevné přiřazení předpokládá detailní znalost architektury systému, teoreticky lze dosáhnout vyšší efektivity pevným přiřazením skupin disjunktně prováděných vláken na systémová vlákna. Systémovým vláknům pak lze u některých implementacích přiřadit konkrétní procesory, na kterých mají běžet. V praxi je ovšem vytváření paralelně prováděných skupin vláken podle počtu dostupných procesorů nereálné. Proto se v tomto případě volí spíše pevné přiřazení typu N:N. Zvolený typ přiřazení je závislý na typu aplikace. Pokud je většina vláken použita pro řešení komunikace, či uživatelského rozhraní, dá se očekávat, že budou většinu času zablokována a postačí podstatně menší počet systémových vláken. V tomto případě je z hlediska využití systémových zdrojů daleko efektivnější použít volné přiřazení a ponechat vazbu a alokaci systémových vláken na systému a knihovnách. Pokud se jedná o aplikaci, kde bude většina vláken v daný okamžik aktivní, pak je běh aplikace rychlejší při pevné vazbě na stejný počet systémových vláken. Toto pravidlo je ověřeno testy na dvouprocesorovém systému Sun (obr. 2.).
Následující tabulka shrnuje typu podporovaných zobrazení v dostupných systémech:
OSF DCE | N:1 | POSIX 1003.4a Draft 4 |
AIX 4.1 | N:N | POSIX 1003.4a Draft 7 |
SOLARIS 2.5 | M:N | POSIX 1003.1c |
SVR4.2/MP | M:N | - |
void *func(void *arg);Parametr funkce arg je předán při spuštění vlákna a může obsahovat adresu libovolného objektu v programu. Jedna funkce může být samozřejmě spuštěna vícekrát a může tak řídit činnost několika paralelně prováděných vláken. Pro vytvoření a spuštění nového vlákna je určena funkce pthread_create():
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*func)(void *), void *arg);Parametr thread je návratový, je v něm vrácena identifikace vytvořeného vlákna. Parametr attr definuje atributy vytvořeného vlákna, pro vytvoření vlákna se standardními atributy lze zadat hodnotu NULL. Funkce func je funkce řídící běh vlákna. Nově vytvořené vlákno začne svůj běh vyvoláním funkce func() s parametrem arg. Ukončením funkce func() je automaticky ukončeno vlákno a případně předán stav ukončení. Úspěšnost vytvoření vlákna je signalizována výsledkem funkce pthread_creat(), podobně jako u jiných funkcí norem POSIX 1003. Hodnota 0 znamená úspěšnost, jiná hodnota je kód chyby. Vlákna jsou na úrovni programu identifikovaná typem pthread_t. Je to obvykle ukazatel na strukturu, ve které je zachycen stav a atributy vlákna. Tento typ a další uvedené typy, prototypy funkcí a makra jsou deklarovány v hlavičkovém souboru <pthread.h>, který musí být vložen na začátku programu. Podobně jako u dalších norem POSIX je požadováno, aby před vložením kteréhokoli standardního hlavičkového souboru byl definován symbol _POSIX_C_SOURCE. Pro zapnutí překladu dle normy POSIX 1003.1c musí mít tento symbol hodnotu rovnu nebo větší 199500L. Dostupnost podpory vláken lze ověřit testem existence symbolu _POSIX_THREADS z hlavičkového souboru <limits.h>:
#define _POSIX_C_SOURCE 199500L #include <limits.h> #ifdef _POSIX_THREADSVlákno končí návratem z počáteční funkce vlákna nebo vyvoláním funkce:
#include <pthread.h> #else #error "POSIX threads are not available" #endif
int pthread_exit(void *status);Vrácený stav může být adresou objektu v programu, podobně jako argument počáteční funkce. Objekt ale nesmí být lokální proměnnou vlákna, protože takový objekt přestává po ukončení vlákna existovat. Pro předání číselného výsledku lze přetypovat parametr na typ int (ovšem za předpokladu, že velikost typu int a void * je stejná) a vracet přímo číselný stav. Stav ukončení vlákna může převzít jiné vlákno uvnitř téhož procesu funkcí:
int pthread_join(pthread_t thread, void **status);Pokud vlákno thread již skončilo, funkce pouze převezme stav a tím uvolní datové struktury spojené s vláknem. Pokud vlákno ještě běží, pozastaví tato funkce volající vlákno a čeká na ukončení vlákna thread.
pthread_mutex_t - binární semafor pro vzájemné vyloučení (mutual exclusion) a
pthread_cond_t - podmíněná proměnná (condition variable).
Pro každý synchronizační prostředek jsou definovány funkce pro inicializaci, použití a zrušení. Synchronizační prostředky jsou reprezentovány proměnnou odpovídajícího typu. Tato proměnná musí být před prvním použitím inicializována funkcí pthread_mutex_init(), resp. pthread_cond_init():
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr); int pthread_cond_init(pthread_cond_t *cond, pthread_condattr *attr);Parametr attr umožňuje zadat atributy prostředku, podobně jako při vytváření vláken. Hodnota NULL znamená vytvoření prostředku s implicitními atributy. Proměnná může být také inicializována statickou inicializací:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex);Dvojice funkcí pthread_mutex_lock() a pthread_mutex_unlock() zahajuje a ukončuje sekci kódu, ve které je vzájemně vyloučeno provádění vláken volajících tyto funkce se stejným argumentem mutex. Funkce pthread_mutex_trylock() zkusí získat výhradní přístup a pokud jej nelze získat, protože výlučný přístup má právě nějaké jiné vlákno, vrátí jako výsledek hodnotu EBUSY. Pokud se ji zdaří získat výlučný přístup, vrátí hodnotu 0. Úsek kódu se vzájemným vyloučením provádění vláken slouží k zajištění výhradního přístupu ke sdíleným objektům. Všechna vlákna, která přistupují k některému sdílenému objektu, musí vždy nejprve získat výhradní přístup, pak provést operaci a po ukončení operace uvolnit přístup jiným vláknům. Každému sdílenému objektu odpovídá jedná proměnná typu pthread_mutex_t a každému přístupu ke sdílenému objektu dvojice zahájení a ukončení přístupu:
SomeType var; /* sdílená proměnná */ pthread_mutex_t var_mutex = PTHREAD_MUTEX_INITIALIZER; ... pthread_mutex_lock(&var_mutex); /* získání výhradního přístupu */ ... /* operace se sdílenou proměnnou */ pthread_mutex_unlock(&var_mutex); /* uvolnění přístupu */Implementace vzájemného vyloučení je různě složitá podle rozsahu kontrol, které se provádí při zahájení a ukončení výhradního přístupu. První variantou implementace je zařazení testu, zda o přístup nežádá znovu vlákno, které již výhradní přístup má. Druhou variantou je povolení či nepovolení takovýchto rekurzivních přístupů. Typ prostředku lze nastavit v atributech prostředku:
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, unsigned type)Parametr type nastavuje typ prostředku ve struktuře atributů attr. Strukturu atributů s nastaveným typem pak použijeme při inicializaci prostředku. Typ prostředku je zadáván předdefinovanými konstantami:
PTHREAD_MUTEXTYPE_FAST | Není detekován pokus o rekurzivní přístup. Pokus o získání přístupu z vlákna, které již přístup k prostředku má, způsobí uváznutí (implicitní hodnota atributu). |
PTHREAD_MUTEXTYPE_DEBUG | Není povolen rekurzivní přístup, ale je detekován a signalizován chybou EDEADLK. |
PTHREAD_MUTEXTYPE_RECURSIVE | Je povolen rekurzivní přístup a počítán počet rekurzivních vstupů. Při uvolňování se čítač snižuje a po posledním rekurzivním výstupu je uvolněn výhradní přístup. |
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);Funkce uvolní jednou nedělitelnou operací výhradní přístup strážený proměnnou mutex a zařadí vlákno do fronty vláken, čekajících na splnění podmínky cond. Až některé jiné vlákno oznámí, že je podmínka splněna, bude čekání ukončeno a před návratem z funkce pthread_cond_wait() získá původní vlákno opět výhradní přístup strážený proměnnou mutex. Nedělitelnost operace uvolnění a zahájení čekání je podstatnou vlastností tohoto synchronizačního prostředku. Zaručuje, že v době mezi uvolněním výhradního přístupu k objektu a zahájením čekání se nezmění stav objektu a nebude tedy signalizováno splnění podmínky, protože to by se v tomto okamžiku ztratilo.
Příklad:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; int condition = 0; /* podmínkou je zde stav sdílené proměnné */ ... pthread_mutex_lock(&mutex); while (condition == 0) { /* čekání na splnění podmínky */ pthread_cond_wait(&cond, &mutex); } /* podmínka je nyní splněna, jiné vlákno ji nemůže změnit, protože přístup k podmínce je strážen pomocí mutex */ ... condition = 0; pthread_mutex_unlock(&mutex); /* uvolnění přístupu k objektu */Cyklus testování stavu podmínky a volání pthread_cond_wait() je nezbytný, protože po ukončení čekání může předběhnout odblokované vlákno při získání výhradního přístupu k objektu jiné vlákno a to může stav podmínky změnit. Splnění podmínky může signalizovat kterékoli vlákno funkcemi:
int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond);Funkce pthread_cond_signal() uvolní z čekání pouze jedno vlákno, zatímco funkce pthread_cond_broadcast() uvolní všechna vlákna čekající na danou podmíněnou proměnnou cond. Není chybou, pokud v daný okamžik nečeká na podmíněnou proměnnou žádné vlákno. Programátor musí zajistit, aby při uvolnění vlákna byla podmínka skutečně splněna. Signalizace splnění podmínky by v uvedeném příkladu mohla mít tvar:
pthread_mutex_lock(&mutex); condition = 1; /* změna stavu podmínky */ pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex);Délku čekání na splnění podmínky a uvolnění lze omezit časovým limitem. Čekání s časovým limitem umožňuje funkce pthread_cont_timedwait():
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);Tato funkce má oproti funkci pthread_cond_wait() navíc parametr abstime, který určuje časový limit čekání. Časový limit je zadán absolutním časem, kdy má být čekání ukončeno.
[2] Provenzano, C. A.: Pthreads, http://www.mit.edu:8001/people/proven/pthreads.html
[3] Tamirisa, C.G.: Porting DCE Threads Programs to AIX 4.1, AIXpert, Aug 1994
[4] Tamirisa, C.G.: Introduction to Multithreaded Programming, AIXpert, Nov 1994
[5] Programming with System Calls and Libraries, Novell, 1994