Pthreads (thread-uri POSIX)
Pthreads (thread-uri POSIX)
La începutul programării paralele, producătorii de hardware își implementau propriile versiuni de thread-uri, care difereau considerabil între ele, ceea ce ducea la dificultăți în dezvoltarea de aplicații multi-threaded portabile. Din acest motiv, o interfață standardizată de programare multi-thread a fost necesară, iar acest lucru a fost specificat, pentru sisteme UNIX, de către standardul IEEE POSIX 1003.1c în anul 1995. Astfel, implementările de thread-uri care aderă la acest standard sunt denumite thread-uri POSIX, sau Pthreads, iar astăzi majoritatea furnizorilor de hardware oferă și Pthreads.
Din punct de vedere al unui programator, Pthreads sunt definite ca o mulțime de tipuri și funcții pentru limbajul C, implementate într-un header numit pthread.h și o bibliotecă numită pthread (deși, în unele implementări de Pthreads, această bibliotecă poate fi inclusă într-o altă bibliotecă).
Implementarea unui program paralel folosind Pthreads
Compilare și rulare
Pentru a compila un program care folosește Pthreads, va trebui să link-ăm biblioteca de Pthreads. Rularea programului se realizează ca pentru orice alt program C. De exemplu, dacă folosim compilatorul GCC, putem compila și rula un program Pthreads astfel:
$ gcc -o program program.c -lpthread
./program
Ca să implementăm un program care folosește Pthreads, trebuie să includem header-ul pthread.h.
Crearea și terminarea thread-urilor
Într-un program C cu Pthreads, inițial există un singur fir de execuție, numit thread-ul principal (main thread). Oricare alt fir de execuție trebuie creat și pornit în mod explicit de către programator, acest lucru făcându-se prin intermediul funcției pthread_create, care poate fi apelată de oricâte ori și de oriunde din cod. Funcția are următoarea semnătură:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
Parametrul thread
(de tip pthread_t
) reprezintă un identificator pentru noul thread returnat de această funcție, ce poate fi apoi folosit pentru a opri sau a referi thread-ul. Parametrul attr
este utilizat pentru a seta diferite atribute pentru firul de execuție care se creează, și poate fi pus pe NULL dacă se dorește păstrarea valorilor implicite. start_routine
este un pointer la funcția care va fi executată pe firul de execuție nou-creat la pornirea sa. În final, prin intermediul parametrului arg
, putem trimite un singur parametru către funcția de thread, care trebuie pasat prin referință ca un pointer de tipul void
(dacă nu se dorește trimiterea unui parametru, putem lăsa NULL
). Funcția returnează 0 dacă thread-ul nou s-a creat și s-a pornit cu succes, sau un cod de eroare în caz contrar.
Un thread creat astfel poate la rândul lui crea alte thread-uri, neexistând o ierarhie sau dependență între ele. Numărul maxim de thread-uri care poate fi creat de un proces depinde de implementarea Pthreads, dar în general nu se recomandă să avem un număr de fire de execuție mai mare decât numărul de core-uri de pe mașina pe care rulăm, din motive de overhead (aceasta este o discuție mai lungă pe care vă recomandăm să o purtați cu asistentul vostru).
Funcția pe care un thread o execută atunci când apelăm pthread_create trebuie să aibă următoarea semnătură:
void *f(void *arg) {
[...]
return NULL;
/*
* In urma ieșirii din funcție, se va apela implicit pthread_exit(NULL);
*/
}
Parametrul arg
al funcției de mai sus este primit de funcția f la execuția ei pe un thread nou atunci când se apelează pthread_create, fiind echivalentului parametrului arg
de la pthread_create. Limitarea dată de faptul că putem trimite un singur parametru funcției de thread poate fi rezolvată prin crearea unei structuri cu oricâți membri, pe care o putem apoi da ca parametru prin referință. Funcția pthread_exit poate fi apelată pentru a termina thread-ul care o apelează, și primește ca parametru fie NULL
, fie o valoare de retur care va fi pasată mai departe. Apelul funcției pthread_exit nu este obligatoriu, pentru că ea este apelata implicit la ieșirea din subrutină, iar parametrul acesteia va fi egal cu valoarea de retur a funcției.
În momentul în care apelăm pthread_create, firul de execuție nou-creat va rula în paralel cu thread-ul principal. Acest lucru înseamnă că tot codul de după apelul funcției pthread_create se va executa în paralel cu codul noului thread. Dacă dorim ca un fir de execuție să aștepte terminarea unui alt thread, putem realiza acest lucru prin funcția pthread_join. Prin apelul acestei funcții, ne putem asigura că thread-ul apelant se blochează până când celălalt fir de execuție își termină procesarea (adică termină de executat funcția de thread). Funcția pthread_join are următoarea semnătură:
int pthread_join(pthread_t thread, void **retval);
Parametrul thread
reprezintă identificatorul thread-ului pe care îl așteptăm, iar retval
reprezintă valoarea de retur a funcției thread-ului așteptat (și poate fi pus pe NULL
dacă nu avem nevoie de această informație). Funcția returnează 0 în caz de succes sau un cod de eroare în caz contrar.
Un exemplu complet de program C care folosește Pthreads se poate observa mai jos. În acest exemplu, se creează două fire de execuție care vor rula aceeași funcție f, dar fiecare din ele va primi ca parametru ID-ul curent (0 sau 1). După ce pornește cele două fire de execuție noi, thread-ul principal le așteaptă pe ambele să termine, după care iese și el.
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_THREADS 2
void *f(void *arg)
{
long id = *(long*) arg;
printf("Hello World din thread-ul %ld!\n", id);
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t threads[NUM_THREADS];
int r;
long id;
void *status;
long arguments[NUM_THREADS];
for (id = 0; id < NUM_THREADS; id++) {
arguments[id] = id;
r = pthread_create(&threads[id], NULL, f, (void *) &arguments[id]);
if (r) {
printf("Eroare la crearea thread-ului %ld\n", id);
exit(-1);
}
}
for (id = 0; id < NUM_THREADS; id++) {
r = pthread_join(threads[id], &status);
if (r) {
printf("Eroare la asteptarea thread-ului %ld\n", id);
exit(-1);
}
}
return 0;
}
O reprezentare vizuală a execuției programului de mai sus poate fi observată în figura de mai jos.
În figura de deasupra, se poate observa că inițial există un singur fir de execuție, cel principal (marcat cu Tmain). În momentul în care acesta apelează pthread_create de două ori, el creează și pornește alte două fire de execuție noi (T0 și T1) care vor rula funcția f cu parametri diferiți (0, respectiv 1). În acest moment, avem trei thread-uri care rulează în paralel. Când thread-ul principal apelează pthread_join, acesta se blochează până când cele două fire de execuție secundare se termină (adică până când termină de executat funcția f). Dacă unul din thread-urile secundare și-a terminat execuția înainte de apelul pthread_join, funcția va returna instant. La finalul programului, thread-ul principal se va opri și el.