În laboratoarele anterioare a fost prezentat conceptul de proces, acesta fiind unitatea elementară de alocare a resurselor utilizatorilor. În cadrul acestui laborator este prezentat conceptul de fir de execuție (sau thread), acesta fiind unitatea elementară de planificare într-un sistem. Ca și procesele, firele de execuție reprezintă un mecanism prin care un calculator poate sǎ ruleze mai multe task-uri simultan.
Un fir de execuție există în cadrul unui proces, și reprezintă o unitate de execuție mai fină decât acesta. În momentul în care un proces este creat, în cadrul lui există un singur fir de execuție, care execută programul secvențial. Acest fir poate la rândul lui sǎ creeze alte fire de execuție; aceste fire vor rula porțiuni ale binarului asociat cu procesul curent, posibil aceleași cu firul inițial (care le-a creat).
.heap
, .data
și .bss
(deci și variabilele stocate în ele)/dev/sda1
).(E)IP
)Procesele sunt folosite de SO pentru a grupa și aloca resurse, iar firele de execuție pentru a planifica execuția de cod care accesează (în mod partajat) aceste resurse.
Deoarece toate firele de execuție ale unui proces folosesc spațiul de adrese al procesului de care aparțin, folosirea lor are o serie de avantaje:
Firele de execuție se pot dovedi utile în multe situații, de exemplu, pentru a îmbunătăți timpul de răspuns al aplicațiilor cu interfețe grafice (GUI), unde prelucrările CPU-intensive se fac de obicei într-un fir de execuție diferit de cel care afișează interfața.
De asemenea, ele simplifică structura unui program și conduc la utilizarea unui număr mai mic de resurse (pentru că nu mai este nevoie de diversele forme de IPC pentru a comunica).
Din punctul de vedere al implementării, există 3 categorii de fire de execuție:
În ceea ce privește firele de execuție, POSIX nu specifică dacă acestea trebuie implementate în user-space sau kernel-space. Linux le implementează în kernel-space, dar nu diferențiază firele de execuție de procese decât prin faptul că firele de execuție partajează spațiul de adresă (atât firele de execuție, cât și procesele, sunt un caz particular de “task”). Pentru folosirea firelor de execuție în Linux trebuie să includem header-ul pthread.h
(unde se găsesc declarațiile funcțiilor și tipurilor de date necesare) și să utilizăm biblioteca libpthread
.
Un fir de execuție este creat folosind pthread_create:
int pthread_create(pthread_t *tid, const pthread_attr_t *tattr, void*(*start_routine)(void *), void *arg);
Noul fir creat va avea identificatorul tid
și va rula concurent cu firul de execuție din care a fost creat. Acesta va executa codul specificat de funcția start_routine
căreia i se va pasa argumentul arg
. Dacă funcția de executat are nevoie de mai mulți parametri, aceștia pot fi agregați într-o structură, în câmpul arg
punându-se un pointer către acea structură.
Prin parametrul tattr
se stabilesc atributele noului fir de execuție. Dacă transmitem valoarea NULL
firul de execuție va fi creat cu atributele implicite.
Pentru a determina identificatorul firului de execuție curent se poate folosi funcția pthread_self:
pthread_t pthread_self(void);
Firele de execuție se așteaptă folosind funcția pthread_join:
int pthread_join(pthread_t th, void **thread_return);
Primul parametru specifică identificatorul firului de execuție așteptat, iar al doilea parametru specifică unde se va plasa valoarea întoarsă de funcția copil (printr-un pthread_exit sau printr-un return
din rutina utilizată la pthread_create).
Firele de execuție se împart în două categorii: unificabile și detașabile. Mai multe detalii:
Un fir de execuție își încheie execuția:
void pthread_exit(void *retval);
Prin parametrul retval
se comunică părintelui un mesaj despre modul de terminare al copilului. Această valoare va fi preluată de funcția pthread_join.
Metodele ca un fir de execuție să termine un alt fir sunt:
libpthread
. Totuși, această metodă nu este recomandată, pentru că este greoaie, și pune probleme foarte delicate la clean-up. Pentru mai multe detalii: Terminarea thread-urilorUneori este util ca o variabilă să fie specifică unui fir de execuție (invizibilă pentru celelalte fire). Linux permite memorarea de perechi (cheie, valoare) într-o zonă special desemnată din stiva fiecărui fir de execuție al procesului curent. Cheia are același rol pe care o are numele unei variabile: desemnează locația de memorie la care se află valoarea.
Fiecare fir de execuție va avea propria copie a unei “variabile” corespunzătoare unei chei k
, pe care o poate modifica, fără ca acest lucru să fie observat de celelalte fire, sau să necesite sincronizare. De aceea, TSD este folosită uneori pentru a optimiza operațiile care necesită multă sincronizare între fire de execuție: fiecare fir calculează informația specifică, și există un singur pas de sincronizare la sfârșit, necesar pentru reunirea rezultatelor tuturor firelor de execuție.
Cheile sunt de tipul pthread_key_t
, iar valorile asociate cu ele, de tipul generic void *
(pointeri către locația de pe stivă unde este memorată variabila respectivă). Descriem în continuare operațiile disponibile cu variabilele din TSD:
O variabilă se creează folosind pthread_key_create:
int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *));
Al doilea parametru reprezintă o funcție de cleanup. Acesta poate avea una din valorile:
NULL
și este ignoratPentru ștergerea unei variabile se apelează pthread_key_delete:
int pthread_key_delete(pthread_key_t key);
Funcția nu apelează funcția de cleanup asociată variabilei.
După crearea cheii, fiecare fir de execuție poate modifica propria copie a variabilei asociate folosind funcția pthread_setspecific:
int pthread_setspecific(pthread_key_t key, const void *pointer);
Pentru a determina valoarea unei variabile de tip TSD se folosește funcția pthread_getspecific:
void* pthread_getspecific(pthread_key_t key);
Funcțiile de cleanup asociate TSD-urilor pot fi foarte utile pentru a asigura faptul că resursele sunt eliberate atunci când un fir se termină singur sau este terminat de către un alt fir. Uneori poate fi util să se poată specifica astfel de funcții fără a crea neapărat un TSD. Pentru acest scop există funcțiile de cleanup.
Atributele reprezintă o modalitate de specificare a unui comportament diferit de comportamentul implicit. Atunci când un fir de execuție este creat cu pthread_create
se poate specifica un atribut pentru respectivul fir de execuție. Atributele implicite sunt suficiente pentru marea majoritate a aplicațiilor. Cu ajutorul unui atribut se pot schimba:
Mai multe detalii puteți găsi în secțiunea suplimentară dedicată.
Un fir de execuție cedează dreptul de execuție unui alt fir, în urma unuia din următoarele evenimente:
int sched_yield(void);
Dacă există alte procese interesate de procesor, unul dintre procese va acapara procesorul, iar dacă nu există nici un alt proces în așteptare pentru procesor, firul curent își continuă execuția.
Dacă dorim să fim siguri că un cod de inițializare se execută o singură dată putem folosi funcția :
pthread_once_t once_control = PTHREAD_ONCE_INIT; int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));
Scopul funcției pthread_once
este de a asigura că o bucată de cod (de obicei folosită pentru inițializări) se execută o singură dată. Argumentul once_control
este un pointer la o variabilă inițializată cu PTHREAD_ONCE_INIT
. Prima oară când această funcție este apelată ea va apela funcția init_routine
și va schimba valoarea variabilei once_control
pentru a ține minte că inițializarea a avut loc. Următoarele apeluri ale acestei funcții cu același once_control
nu vor face nimic.
Funcția pthread_once
întoarce întotdeauna 0.
Pentru a determina dacă doi identificatori se referă la același fir de execuție se poate folosi :
int pthread_equal(pthread_t thread1, pthread_t thread2);
Pentru aflarea/modificarea priorităților sunt disponibile următoarele apeluri :
int pthread_setschedparam(pthread_t target_thread, int policy, const struct sched_param *param); int pthread_getschedparam(pthread_t target_thread, int *policy, struct sched_param *param);
La compilare trebuie specificată și biblioteca libpthread
(deci se va folosi argumentul -lpthread
).
Atenție! Nu legați un program single-threaded cu această bibliotecă. Anumite apeluri din bibliotecile standard pot avea implementări mai ineficiente sau mai greu de depanat când se utilizează această bibliotecă.
În continuare, este prezentat un exemplu simplu în care sunt create 2 fire de execuție, fiecare afișând un caracter de un anumit număr de ori pe ecran.
#include <pthread.h> #include <stdio.h> /* parameter structure for every thread */ struct parameter { char character; /* printed character */ int number; /* how many times */ }; /* the function performed by every thread */ void* print_character(void *params) { struct parameter* p = (struct parameter*) params; int i; for (i=0;i<p->number;i++) printf("%c", p->character); printf("\n"); return NULL; } int main() { pthread_t fir1, fir2; struct parameter fir1_args, fir2_args; /* create one thread that will print 'x' 11 times */ fir1_args.character = 'x'; fir1_args.number = 11; if (pthread_create(&fir1, NULL, &print_character, &fir1_args)) { perror("pthread_create"); exit(1); } /* create one thread that will print 'y' 13 times */ fir2_args.character = 'y'; fir2_args.number = 13; if (pthread_create(&fir2, NULL, &print_character, &fir2_args)) { perror("pthread_create"); exit(1); } /* wait for completion */ if (pthread_join(fir1, NULL)) perror("pthread_join"); if (pthread_join(fir2, NULL)) perror("pthread_join"); return 0; }
Comanda utilizată pentru a compila acest exemplu va fi:
gcc -o exemplu exemplu.c -lpthread
Pentru sincronizarea firelor de execuție, avem la dispoziție:
Mutex-urile (mutual exclusion locks) sunt obiecte de sincronizare utilizate pentru a asigura accesul exclusiv într-o secțiune de cod în care se utilizează date partajate între două sau mai multe fire de execuție. Un mutex are două stări posibile: ocupat și liber. Un mutex poate fi ocupat de un singur fir de execuție la un moment dat. Atunci când un mutex este ocupat de un fir de execuție, el nu mai poate fi ocupat de niciun alt fir. În acest caz, o cerere de ocupare venită din partea unui alt fir, în general, va bloca firul până în momentul în care mutex-ul devine liber.
Un mutex poate fi inițializat/distrus în mai multe moduri:
// initializare statica a unui mutex, cu atribute implicite // NB: mutex-ul nu este eliberat, durata de viata a mutex-ului // este durata de viata a programului. pthread_mutex_t mutex_static = PTHREAD_MUTEX_INITIALIZER;
// semnaturile functiilor de initializare si distrugere de mutex: int pthread_mutex_init (pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); int pthread_mutex_destroy(pthread_mutex_t *mutex); void initializare_mutex_cu_atribute_implicite() { pthread_mutex_t mutex_implicit; pthread_mutex_init(&mutex_implicit, NULL); // atrr=NULL -> atribute implicite // ... folosirea mutex-ului ... // eliberare mutex pthread_mutex_destroy(&mutex_implicit); }
Atenție: Mutex-ul trebuie să fie liber pentru a putea fi distrus. În caz contrar, funcția va întoarce codul de eroare EBUSY
. Întoarcerea valorii 0
semnifică succesul apelului.
Folosind atributele de inițializare se pot crea mutex-uri cu proprietăți speciale:
PTHREAD_PRIO_NONE
– nu se moștenește prioritatea când deținem mutex-ul creat cu acest atributPTHREAD_PRIO_INHERIT
– dacă deținem un mutex creat cu acest atribut și dacă există fire de execuție blocate pe acel mutex, se moștenește prioritatea firului de execuție cu cea mai mare prioritatePTHREAD_PRIO_PROTECT
– dacă firul de execuție curent deține unul sau mai multe mutex-uri, acesta va executa la maximul priorităților specificate pentru toate mutex-urile deținute. Funcțiile de ocupare blocantă/eliberare a unui mutex (pthread_mutex_lock, pthread_mutex_unlock):
int pthread_mutex_lock (pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);
Dacă mutex-ul este liber în momentul apelului, acesta va fi ocupat de firul apelant și funcția va întoarce imediat. Dacă mutex-ul este ocupat de un alt fir, apelul va bloca până la eliberarea mutex-ului. Dacă mutex-ul este deja ocupat de firul curent de execuție (lock recursiv), comportamentul funcției este dictat de tipul mutex-ului:
Tip mutex | Lock recursiv | Unlock |
---|---|---|
PTHREAD_MUTEX_NORMAL | deadlock | eliberează mutex-ul |
PTHREAD_MUTEX_ERRORCHECK | returnează eroare | eliberează mutex-ul |
PTHREAD_MUTEX_RECURSIVE | incrementează contorul de ocupări | decrementează contorul de ocupări (la zero eliberează mutex-ul) |
PTHREAD_MUTEX_DEFAULT | deadlock | eliberează mutex-ul |
Nu este garantată o ordine FIFO de ocupare a unui mutex. Oricare din firele aflate în așteptare la deblocarea unui mutex pot să-l acapareze.
Pentru a încerca ocuparea unui mutex fără a aștepta eliberarea acestuia în cazul în care este deja ocupat, se va apela funcția pthread_mutex_trylock:
int pthread_mutex_trylock(pthread_mutex_t *mutex);
Exemplu:
int rc = pthread_mutex_trylock(&mutex); if (rc == 0) { /* successfully aquired the free mutex */ } else if (rc == EBUSY) { /* mutex was held by someone else instead of blocking we return EBUSY */ } else { /* some other error occured */ }
Un exemplu de utilizare a unui mutex pentru a serializa accesul la variabila globală global_counter
:
#include <stdio.h> #include <pthread.h> #define NUM_THREADS 5 /* global mutex */ pthread_mutex_t mutex; int global_counter = 0; void *thread_routine(void *arg) { /* acquire global mutex */ pthread_mutex_lock(&mutex); /* print and modify global_counter */ printf("Thread %d says global_counter=%d\n", (int) arg, global_counter); global_counter++; /* release mutex - now other threads can modify global_counter */ pthread_mutex_unlock(&mutex); return NULL; } int main(void) { int i; pthread_t tids[NUM_THREADS]; /* init mutex once, but use it in every thread */ pthread_mutex_init(&mutex, NULL); /* all threads execute thread_routine as args to the thread send a thread id represented by a pointer to an integer */ for (i = 0; i < NUM_THREADS; i++) pthread_create(&tids[i], NULL, thread_routine, (void *) i); /* wait for all threads to finish */ for (i = 0; i < NUM_THREADS; i++) pthread_join(tids[i], NULL); /* dispose mutex */ pthread_mutex_destroy(&mutex); return 0; }
so@spook$ gcc -Wall mutex.c -lpthread so@spook$ ./a.out Thread 1 says global_counter=0 Thread 2 says global_counter=1 Thread 3 says global_counter=2 Thread 4 says global_counter=3 Thread 0 says global_counter=4
Mutex-urile din firele de execuție POSIX sunt implementate cu ajutorul futex-urilor, din considerente de performanță.
Optimizarea constă în testarea și setarea atomică a valorii mutex-ului (printr-o instrucțiune de tip test-and-set-lock) în user-space, eliminându-se trap-ul în kernel în cazul în care nu este necesară blocarea.
Semafoarele sunt obiecte de sincronizare ce reprezintă o generalizare a mutexurilor prin aceea că salvează numărul de operații de eliberare (incrementare) efectuate asupra lor. Practic, un semafor reprezintă un întreg care se incrementează/decrementează atomic. Valoarea unui semafor nu poate scădea sub 0. Dacă semaforul are valoarea 0, operația de decrementare se va bloca până când valoarea semaforului devine strict pozitivă. Mutexurile pot fi privite, așadar, ca niște semafoare binare.
Semafoarele POSIX au fost prezentate în cadrul Laboratorului 05 - IPC.
Operațiile care pot fi efectuate asupra semafoarelor POSIX sunt multiple:
/* use named semaphore to synchronize processes */ /* open */ sem_t* sem_open(const char *name, int oflag); /* create */ sem_t* sem_open(const char *name, int oflag, mode_t mode, unsigned int value); /* closing named semaphore */ int sem_close(sem_t *sem); /* delete from system a names semaphore */ int sem_unlink(const char *name);
int sem_init(sem_t *sem, int pshared, unsigned int value); /* close unnamed semaphore */ int sem_destroy(sem_t *sem);
/* increment/release semaphore (V) */ int sem_post(sem_t *sem); /* decrement/acquire semaphore (P) */ int sem_wait(sem_t *sem); /* non-blocking decrement/acquire */ int sem_trywait(sem_t *sem); /* getting the semaphore count */ int sem_getvalue(sem_t *sem, int *pvalue);
Variabilele condiție pun la dispoziție un sistem de notificare pentru fire de execuție, permițându-i unui fir să se blocheze în așteptarea unui semnal din partea unui alt fir. Folosirea corectă a variabilelor condiție presupune un protocol cooperativ între firele de execuție.
Mutex-urile și semafoarele permit blocarea altor fire de execuție. Variabilele de condiție se folosesc pentru a bloca firul curent până la îndeplinirea unei condiții.
Variabilele condiție sunt obiecte de sincronizare care-i permit unui fir de execuție să-și suspende execuția până când o condiție (predicat logic) devine adevărată. Când un fir de execuție determină că predicatul a devenit adevărat, va semnala variabila condiție, deblocând astfel unul sau toate firele de execuție blocate la acea variabilă condiție (în funcție de intenție).
O variabilă condiție trebuie întotdeauna folosită împreună cu un mutex pentru evitarea race-ului care se produce când un fir se pregătește să aștepte la variabila condiție în urma evaluării predicatului logic, iar alt fir semnalizează variabila condiție chiar înainte ca primul fir să se blocheze, pierzându-se astfel semnalul. Așadar, operațiile de semnalizare, testare a condiției logice și blocare la variabila condiție trebuie efectuate având ocupat mutexul asociat variabilei condiție. Condiția logică este testată sub protecția mutexului, iar dacă nu este îndeplinită, firul apelant se blochează la variabila condiție, eliberând atomic mutexul. În momentul deblocării, un fir de execuție va încerca să ocupe mutexul asociat variabilei condiție. De asemenea, testarea predicatului logic trebuie făcută într-o buclă, deoarece, dacă sunt eliberate mai multe fire deodată, doar unul va reuși să ocupe mutexul asociat condiției. Restul vor aștepta ca acesta să-l elibereze, însă este posibil ca firul care a ocupat mutexul să schimbe valoarea predicatului logic pe durata deținerii mutexului. Din acest motiv celelalte fire trebuie să testeze din nou predicatul pentru că, altfel, și-ar începe execuția presupunând predicatul adevărat, când el este, de fapt, fals.
Inițializarea unei variabile de condiție se face folosind macro-ul PTHREAD_COND_INITIALIZER sau funcția pthread_cond_init. Distrugerea unei variabile de condiție se face prin funcția pthread_cond_destroy.
// initializare statica a unei variabile de condiție cu atribute implicite // NB: variabila de conditie nu este eliberata, // durata de viata a variabilei de condiție este durata de viata a programului. pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // semnaturile functiilor de initializare si eliberare de variabile de condiție: int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr); int pthread_cond_destroy(pthread_cond_t *cond);
Ca și la mutex-uri:
attr
este nul, se folosesc atribute impliciteEBUSY
. Pentru a-și suspenda execuția și a aștepta la o variabilă condiție, un fir de execuție va apela funcția pthread_cond_wait:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
Firul de execuție apelant trebuie să fi ocupat deja mutexul asociat, în momentul apelului. Funcția pthread_cond_wait
va elibera mutexul și se va bloca, așteptând ca variabila condiție să fie semnalizată de un alt fir de execuție. Cele două operații sunt efectuate atomic. În momentul în care variabila condiție este semnalizată, se va încerca ocuparea mutexului asociat, și după ocuparea acestuia, apelul funcției va întoarce. Observați că firul de execuție apelant poate fi suspendat, după deblocare, în așteptarea ocupării mutexului asociat, timp în care predicatul logic, adevărat în momentul deblocării firului, poate fi modificat de alte fire. De aceea, apelul pthread_cond_wait
trebuie efectuat într-o buclă în care se testează valoarea de adevăr a predicatului logic asociat variabilei condiție, pentru a asigura o serializare corectă a firelor de execuție. Un alt argument pentru testarea în buclă a predicatului logic este acela că un apel pthread_cond_wait
poate fi întrerupt de un semnal asincron (vezi laboratorul de semnale), înainte ca predicatul logic să devină adevărat. Dacă firele de execuție care așteptau la variabila condiție nu ar testa din nou predicatul logic, și-ar continua execuția presupunând greșit că acesta e adevărat.
Pentru a-și suspenda execuția și a aștepta la o variabilă condiție, nu mai târziu de un moment specificat de timp, un fir de execuție va apela pthread_cond_timedwait:
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
Funcția se comportă la fel ca pthread_cond_wait
, cu excepția faptului că, dacă variabila condiție nu este semnalizată mai devreme de abstime
, firul apelant este deblocat, și, după ocuparea mutexului asociat, funcția se întoarce cu eroarea ETIMEDOUT
. Parametrul abstime
este absolut și reprezintă numărul de secunde trecute de la 1 ianuarie 1970, ora 00:00.
Pentru a debloca un singur fir de execuție blocat la o variabilă condiție se va semnaliza variabila condiție folosind pthread_cond_signal:
int pthread_cond_signal(pthread_cond_t *cond);
Dacă la variabila condiție nu așteaptă niciun fir de execuție, apelul funcției nu are efect și semnalizarea se va pierde. Dacă la variabila condiție așteaptă mai multe fire de execuție, va fi deblocat doar unul dintre acestea. Alegerea firului care va fi deblocat este făcută de planificatorul de fire de execuție. Nu se poate presupune că firele care așteaptă vor fi deblocate în ordinea în care și-au început așteptarea. Firul de execuție apelant trebuie să dețină mutexul asociat variabilei condiție în momentul apelului acestei funcții.
Exemplu:
pthread_mutex_t count_lock; pthread_cond_t count_nonzero; unsigned count; void decrement_count() { pthread_mutex_lock(&count_lock); while (count == 0) pthread_cond_wait(&count_nonzero, &count_lock); count = count - 1; pthread_mutex_unlock(&count_lock); } void increment_count() { pthread_mutex_lock(&count_lock); while (count > 0) pthread_cond_signal(&count_nonzero); count = count + 1; pthread_mutex_unlock(&count_lock); }
Pentru a debloca toate firele de execuție blocate la o variabilă condiție, se semnalizează variabila condiție folosind pthread_cond_broadcast:
int pthread_cond_broadcast(pthread_cond_t *cond);
Dacă la variabila condiție nu așteaptă niciun fir de execuție, apelul funcției nu are efect și semnalizarea se va pierde. Dacă la variabila condiție așteaptă fire de execuție, toate acestea vor fi deblocate, dar vor concura pentru ocuparea mutexului asociat variabilei condiție. Firul de execuție apelant trebuie să dețină mutexul asociat variabilei condiție în momentul apelului acestei funcții.
În următorul program se utilizează o barieră pentru a sincroniza firele de execuție ale programului. Bariera este implementată cu ajutorului unei variabile de condiție.
#include <stdio.h> #include <pthread.h> #define NUM_THREADS 5 // implementarea unei bariere *nereentrante* cu variabile de conditie struct my_barrier_t { // mutex folosit pentru a serializa accesele la datele interne ale barierei pthread_mutex_t lock; // variabila de conditie pe care se astepta sosirea tuturor firelor de executie pthread_cond_t cond; // numar de fire de executie care trebuie sa mai vina pentru a elibera bariera int nr_still_to_come; }; struct my_barrier_t bar; void my_barrier_init(struct my_barrier_t *bar, int nr_still_to_come) { pthread_mutex_init(&bar->lock, NULL); pthread_cond_init(&bar->cond, NULL); // cate fire de executie sunt asteptate la bariera. bar->nr_still_to_come = nr_still_to_come; } void my_barrier_destroy(struct my_barrier_t *bar) { pthread_cond_destroy(&bar->cond); pthread_mutex_destroy(&bar->lock); } void *thread_routine(void *arg) { int thd_id = (int) arg; // inainte de a lucra cu datele interne ale barierei trebuie sa preluam mutexul pthread_mutex_lock(&bar.lock); printf("thd %d: before the barrier\n", thd_id); // suntem ultimul fir de executie care a sosit la bariera? int is_last_to_arrive = (bar.nr_still_to_come == 1); // decrementam numarul de fire de executie asteptate la bariera bar.nr_still_to_come --; // cat timp mai sunt fire de execuție care nu au ajuns la bariera, asteptam. while (bar.nr_still_to_come != 0) // lockul se elibereaza automat inainte de a incepe asteptarea pthread_cond_wait(&bar.cond, &bar.lock); // ultimul fir de execuție ajuns la bariera va semnaliza celelalte fire if (is_last_to_arrive) { printf(" let the flood in\n"); pthread_cond_broadcast(&bar.cond); } printf("thd %d: after the barrier\n", thd_id); // la iesirea din functia de asteptare se preia automat mutexul, trebuie eliberat. pthread_mutex_unlock(&bar.lock); return NULL; } int main(void) { int i; pthread_t tids[NUM_THREADS]; my_barrier_init(&bar, NUM_THREADS); for (i = 0; i < NUM_THREADS; i++) pthread_create(&tids[i], NULL, thread_routine, (void *) i); for (i = 0; i < NUM_THREADS; i++) pthread_join(tids[i], NULL); my_barrier_destroy(&bar); return 0; }
so@spook$ gcc -Wall cond_var.c -pthread so@spook$ ./a.out thd 0: before the barrier thd 2: before the barrier thd 3: before the barrier thd 4: before the barrier thd 1: before the barrier let the flood in thd 1: after the barrier thd 2: after the barrier thd 3: after the barrier thd 4: after the barrier thd 0: after the barrier
Din execuția programului se observă:
Standardul POSIX definește și un set de funcții și structuri de date de lucru cu bariere. Aceste funcții sunt disponibile dacă se definește macro-ul _XOPEN_SOURCE
la o valoare >= 600.
Bariera se va inițializa folosind pthread_barrier_init și se va distruge folosind pthread_barrier_destroy.
// pentru a folosi funcțiile de lucru cu bariere e nevoie să se definească // _XOPEN_SOURCE la o valoare >= 600. Pentru detalii consultați feature_test_macros(7). #define _XOPEN_SOURCE 600 #include <pthread.h> // attr -> un set de adribute, poate fi NULL (se folosesc atribute implicite) // count -> numărul de fire de execuție care trebuie să ajungă // la barieră pentru ca aceasta să fie eliberată int pthread_barrier_init(pthread_barrier_t * barrier, const pthread_barrierattr_t * attr, unsigned count); // trebuie să nu existe fire de execuție în așteptare la barieră // înainte de a apela funcția _destroy, altfel, se întoarce EBUSY // și nu se distruge bariera. int pthread_barrier_destroy(pthread_barrier_t *barrier);
Așteptarea la barieră se face prin apelul pthread_barrier_wait:
#define _XOPEN_SOURCE 600 #include <pthread.h> int pthread_barrier_wait(pthread_barrier_t *barrier);
Dacă bariera a fost creată cu count=N
, primele N-1
fire de execuție care apelează pthread_barrier_wait
se blochează. Când sosește ultimul (al N
-lea), va debloca toate cele N-1
fire de execuție. Funcția pthread_barrier_wait
întoarce trei valori:
EINVAL
– în cazul în care bariera nu este inițializată (singura eroare definită)PTHREAD_BARRIER_SERIAL_THREAD
– în caz de succes, un singur fir de execuție va întoarce valoarea aceasta – nu e specificat care este acel fir de execuție (nu e obligatoriu să fie ultimul ajuns la barieră)0
– valoare întoarsă în caz de succes de celelalte N-1
fire de execuție. Cu bariere POSIX, programul de mai sus poate fi simplificat:
#define _XOPEN_SOURCE 600 #include <pthread.h> #include <stdio.h> #define NUM_THREADS 5 pthread_barrier_t barrier; void *thread_routine(void *arg) { int thd_id = (int) arg; int rc; printf("thd %d: before the barrier\n", thd_id); // toate firele de executie asteapta la bariera. rc = pthread_barrier_wait(&barrier); if (rc == PTHREAD_BARRIER_SERIAL_THREAD) { // un singur fir de execuție (posibil ultimul) va intoarce PTHREAD_BARRIER_SERIAL_THREAD // restul firelor de execuție întorc 0 în caz de succes. printf(" let the flood in\n", thd_id); } printf("thd %d: after the barrier\n", thd_id); return NULL; } int main(void) { int i; pthread_t tids[NUM_THREADS]; // bariera este initializata o singura data si folosita de toate firele de executie pthread_barrier_init(&barrier, NULL, NUM_THREADS); // firele de executie vor executa codul functiei 'thread_routine'. // in locul unui pointer la date utile, se trimite in ultimul argument // un intreg - identificatorul firului de executie for (i = 0; i < NUM_THREADS; i++) pthread_create(&tids[i], NULL, thread_routine, (void *) i); // asteptam ca toate firele de executie sa se termine for (i = 0; i < NUM_THREADS; i++) pthread_join(tids[i], NULL); // eliberam resursele barierei pthread_barrier_destroy(&barrier); return 0; }
so@spook$ gcc -Wall barrier.c -lpthread so@spook$ ./a.out thd 0: before the barrier thd 2: before the barrier thd 1: before the barrier thd 3: before the barrier thd 4: before the barrier let the flood in thd 4: after the barrier thd 2: after the barrier thd 3: after the barrier thd 0: after the barrier thd 1: after the barrier
În rezolvarea laboratorului folosiți arhiva de sarcini lab08-tasks.zip
Observații: Pentru a vă ajuta la implementarea exercițiilor din laborator, în directorul utils
din arhivă există un fișier utils.h
cu funcții utile.
sudo apt-get install manpages-posix manpages-posix-dev
1-th_stack
și inspectați sursa.pmap
cum se modifică spațiul de adresă al programului.watch -d pmap $(pidof th_stack)
pthread_create
?Ctrl+C
pentru a termina programul. 2-th_vs_proc
și inspectați sursele.init
?ps
.do_bad_task
la fiecare al 4-lea fir de execuție/process.3-safety
și inspectați sursa vars.c
thread_function
și main
thread-safe relativ la variabilele a, b, c? thread safe
.rez
după ce se face join. Cum explicați?malloc
thread-safe?malloc.c
testează apelul malloc realizat din mai multe fire de execuție.pfgrep.c
din directorul 4-pfgrep
.ls -R / > big_file.txt
time
.Blocked
blocked.c
din directorul 5-blocked
, compilați și executați binarul (repetați până detectați blocarea programului). found
al structurii celuilalt fir de execuție, pentru a vedea dacă acesta a găsit deja numărul căutat. helgrind
, unul din tool-urile valgrind
, pentru a detecta problema: $ valgrind --tool=helgrind ./blocked
pthread_once()
once.c
din directorul 6-once
, asigurați-vă că funcția init_func()
este apelată o singură dată.init_func()
si nu folositi pthread_once()
.7-prodcons
9-tsd/tsd.c
conține o aplicație ce împarte un task între mai multe fire de execuție.thread_log_key
.close_thread_log
.thread_function
nu mai trebuie să închidă fișierele de log?10-spin
și inspectați sursa spin.c