Vlákna a Linux

Tomáš Plch  /  27. 06. 2006, 00:00

V dnešnej časti si povieme o vláknach v operačnom systéme Linux. Naučíme sa vytvárať vlákna a narábať s nimi, joinovať i detachovať vlákna. Taktiež si povieme niečo o chovaní vláken a nebudú chybať ani príklady.

V dnešnej časti si povieme o používaní vláken pod operačným systémom Linux. Uvedieme si aj konkrétne príklady. Operačný systém Linux plánuje vlákna, pričom zohľadňuje viaceré kritéria (Nice hodnota procesu, priorita...), avšak skúmanie plánovacieho procesu Linuxu je nad záber tohoto článku.

V prípade vláken v Linuxe je dôležité si povedať niečo o stavoch. Vlákno sa podobne ako proces, môže nachádzať v stavoch ako running, ready, asleep (blocked) a v stave podobnom stavu zombie. Vlákna sú joinable a detached. Čo tieto dva stavy znamenajú? Vlákno má pridelené určité prostriedky (vlastný zásobník, pamäť (heap)...) a tieto prostriedky treba po ukončení vlákna (návratu z hlavnej rutiny vlákna) uvolniť. Otázka je -  "KEDY?". V prípade, že je vlákno v detached stave, tak sa jeho prostriedky uvoľnia hneď po jeho ukončení (prípadne zrušení). Z takto ukončeného vlákna sa nedá získať návratová hodnota.

Na druhú stranu existuje stav joinable. V tomto stave vlákno po ukončení ostane a čaká na tzv. joinutie (pripojenie). Iné vlákno pomocou špeciálnej funkcie (o ktorej si povieme neskôr) pripojí vlákno a získa jeho návratovú hodnotu. Prostriedky vlákna sú uvoľnené a vlákno sa môže konečne odobrať na odpočinok. Ak vás napadlo, čo sa stane ak vlákno, ktoré pripája iné vlákna "umrie", prípadne sa dostane napr. do nekonečného cyklu, odpoveď je jednoduchá. Vlákna čakajú až do ukončenia procesu, v ktorého adresovom priestore existujú. Teda sa netreba strachovať, že by vám nekonečne dlho čakajúce vlákna plnili pamäť, a žrali prostriedky.

Aby sme trošku prehĺbli predstavu o vláknach, povieme si čo majú thready jedného procesu spoločné - adresový priestor, user ID, group ID, deskriptory, signály, aktuálny adresár (určite som na niečo zabudol, ale tá idea tu je). A povieme si, čo naopak majú vlákna unikátne - identifikátor (ID), zásobník, určité registre procesoru (register vrcholu zásobníku, PC (ukazateľ na najbližsie vykonávanú inštrukciu)), masku signálov, prioritu a chybovú globálnu premennú errno.

Aby som príliš nezdržoval, hneď sa vrhneme na programovanie. Ako prvé si povieme, čo treba urobiť, aby sme mohli vlákna v operačnom systéme Linux používať. Vo svojich zdrojových súboroch treba zainklúdovať súbor pthread.h. Druhá dôležitá vec je pridať pri kompilácii príznak -lpthread.

Ako prvú si predstavíme funkciu, ktorou sa vlákna vytvárajú.

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

Vysvetlíme si jednotlivé argumenty. Podobne ako v operačnom systéme Windows, je i tu vlákno identifikované špeciálnym typom. Obsahom tohoto typu vás nebudem zaťažovať.

Argument thread je pointer na inštanciu premennej typu pthread_t (tento argument musí byť naalokovaný v pamäti, prípadne musí byť globálny, prípadne lokálny). Tento argument sa naplní údajmi o vytvorenom vlákne.

Argument attr špecifikuje atribúty vytváraného vlákna. Jeho presnú štruktúru môžete nájsť v pthreadtypes.h. Čo je pre nás dôležité je tzv. detached state, ktorému sa daju nastaviť hodnoty PTHREAD_CREATE_DETACHED alebo PTHREAD_CREATE_JOINABLE. Ostané parametre sa dajú dohľadať v dokumentácii (napr. velkosť zásobníku, parametre plánvoania, politka plánovania, a iné ). V prípade, že túto premennú zadáte ako NULL, nič sa nedeje, vlákno sa vytvorí s defaultnými hodnotami atribútov.

Argument start_routine je ukazateľ na funkciu, ktorá reprezentuje vykonávaný kód vlákna. Funkcia musí byť v tvare void* NazovFunkcie(void* param)

Argument arg je pointer, ktorý sa hlavnej funkcii vlákna predá ako parameter.

Ukážeme si jednoduchý kód
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

//pocet vypisov
#define MAX_LOOPS 10000

//vlakna
pthread_t thread_a, thread_b, thread_c;

//funkcia ktora je telom vlakna
void* Test(void* id)
{
int i;
//informacne vypisy
printf ("Thread %d startedn", *(int*)id );

//cyklus vypisania
for (i =0; i< MAX_LOOPS; i++)
{
//prvy thread vypise a druhy b atd
printf("%c",(int)'a'-1+(*(int*)id));
}
return NULL;
}

int main( void )
{
int a = 1, b = 2, c = 3;

//vytvorenie vlaken
pthread_create (&thread_a, NULL, Test, &a);
pthread_create (&thread_b, NULL, Test, &b);
pthread_create (&thread_c, NULL, Test, &c);

//pripojenie vlaken
pthread_join( thread_a, NULL);
pthread_join( thread_b, NULL);
pthread_join( thread_c, NULL);

return 0;
}

Po skompilovaní by ste na obrazovke mali vidieť vypísané postupnosti znakov a, b, c. Keď sa pozrieme presnejšie na výpis, uvidíme (nemusí to byť zákonitosť, záleží to od plánovača a iných parametrov) ako sa medzi výpisy ačok dostali bčka prípadne cčka. Keby sme výpis farebne odlíšili, bolo by vidieť, že jednotlivé sekvencie výpisu nie sú konzistetné. Toto je pekný príklad na to, ako dochádza ku kolízii vláken. Pokiaľ by sme chceli, aby výpis ačok bol neprerušený iným výpisom (rovnako aj výpis bčok a cčok), tak by sme museli vlákna synchronizovať (ale o tom neskôr).

Ako ste si všimli, v kóde sme použili funkciu pthread_join.
Syntax tejto funkcie je nasledovná int pthread_join ( pthread_t th, void ** thread_return)

Pokúsim sa Vám vysvetliť význam jednotlivých argumentov. Argument th odkazuje na vlákno ktoré sa má "pripojiť". Do argumentu thread_return sa naplní obsah návratovej hodnoty hlavnej funkcie vlákna. Pozor, aby návratová hodnota nebola lokálneho charakteru, pretože po uvoľnení prostriedkov vlákna dôjde k jej zániku.

Ako ďalšiu funkciu si predstavíme int pthread_exit( void * retval ).
Táto funkcia sa nikdy nevráti zo svojho volania. Je to logické, pretože zaháji ukončenie vlákna. Argument retval ukazuje na hodnotu, ktorá sa má pri prípadnom pripojení vlákna vrátiť ako návratová hodnota vlákna. Znova si treba dať pozor, na to na akú hodnotu odkazujeme v tomto prípade. Ak bude retval odkazovať na lokálnu premennú vlákna, táto sa stane neexistentnou v dobe, kedy sa budú uvoľnovať prostriedky vlákna.

Ako poslednú funkciu si ukážeme funkciu int pthread_detach ( pthread_t th ). Jediný argument th tejto funkcie je štruktúra typu pthread_t. Táto funkcia nastaví vlákno do stavu detached a potom už vlákno netreba pripájať (join) a teda sa o to vlákno ani netreba už starať z pohľadu iných vlákien. Nevýhodou je to, že nezískame žiadnu návratovu hodnotu z vlákna po jeho ukončení. Aby bolo vlákno detached sa dá docieliť i správnym nastavením atribútov pri vytváraní vlákna.

Jednoduchý príklad použitia na záver. V ďalšej časti si povieme o synchronizácii a synchronizačnom primitíve - mutex.


#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

//pocet vypisov
#define MAX_LOOPS 10000

//vlakna
pthread_t thread_a, thread_b, thread_c;

//funkcia ktora je telom vlakna
void* Test(void* id)
{
int i;
//informacne vypisy
printf ("Thread %d startedn", *(int*)id );

//cyklus vypisania
for (i =0; i< MAX_LOOPS; i++)
{
//prvy thread vypise a druhy b atd
printf("%c",(int)'a'-1+(*(int*)id));
}
return NULL;
}

int main( void )
{
int a = 1, b = 2, c = 3;

//vytvorenie vlaken
pthread_create (&thread_a, NULL, Test, &a);
//detach
pthread_detach ( thread_a );

pthread_create (&thread_b, NULL, Test, &b);
//detach
pthread_detach ( thread_b );

pthread_create (&thread_c, NULL, Test, &c);

//detach
pthread_detach ( thread_c );

return 0;
}
<= predchádzajúci článok     nasledujúci článok =>

Neprehliadnite: