next_inactive up previous


Thread-uri. Thread-uri POSIX (pthreads)

Razvan Deaconescu

26 noiembrie 2005

Pana in momentele de fata s-a discutat foarte mult despre procese in Linux (UNIX): crearea acestora, modalitati de comunicare inter-proces. Uneori insa, overhead-ul crearii unui nou proces folosind fork este considerat mult prea mare. Ar fi de dorit sa realizam un singur program care sa realizeze doua lucruri simultan (sau cel putin aparent sa faca doua lucruri simultan). In aceasta situatie se recomanda utilizarea thread-urilor.

Thread-uri

Mai multe instante de executie in cadrul unui singur program sunt denumite thread-uri (fire de executie). O denumire a thread-urilor este lightweight process. Desi empirica spune destul de multe lucruri despre functionalitatea thread-urilor. O definitie mai precisa ar fi ``un thread este o secventa de control in cadrul unui proces''. Pana acum toate programele rulau in cadrul unui singur proces, iar toate procese de pana acuma aveau un singur thread de executie.

Deosebirea intre apelul fork si crearea unui nou thread este aceea ca apelul fork creeaza o copie identica a procesului parinte si ii stabileste noului proces un identificator propriu. Noul proces este planificat independent, si se executa de obicei intr-un cadru independent de procesul parinte. De partea cealalta crearea unui thread este mult mai putin costisitoare. Crearea unui nou thread in cadrul unui proces inseamna crearea unei stive locale (pentru variabile proprii) si a unui set propriu de registre. In rest informatiile sunt partajate: variabilele globale, descriptorii de fisier, handler-ele de semnale, etc.

Avantaje si dezavantaje ale folosirii thread-urilor

Crearea unui nou thread este eficienta din punct de vedere al costului si overhead-ului implicat. In particular, in Linux se creeaza foarte rapid chiar si procese si diferenta nu este foarte mare.

De multe ori, este de dorit ca un program sa faca (sau cel putin sa para ca face) doua lucruri in acelasi timp. Un caz este editarea text-ului unui document cu mentinerea simultana a informatiilor despre acesta (numar de cuvinte). Un thread se poate ocupa de intrarea utilizatorului si de editare. Altul se ocupa de update-ul numarului de cuvinte. Un exemplu mai realist este utilizarea unui sistem de baza de date multithreaded. Aici un proces server deserveste mai multi clienti, si mareste productivitatea prin deserverirea unor cereri in timp ce altele sunt blocate.

Thread-urile au si dezavantaje. Deoarece ele partajeaza o mare parte din resursele unui proces, scrierea de programe multithreaded solicita un efort de planificare suplimentar. Pot aparea inconsistente in situatia in care nu am realizat sincronizarea anumitor variabile. Depanarea programelor multithreaded este, de asemenea, mai dificila.

Un program care foloseste thread-uri pentru rezolvarea unei probleme de calcul intens nu va rula mai rapid pe un sistem uniprocesor.

Totusi, scrierea unei aplicatii care realizeaza o mixare a intrarii, calculului si iesirii poate fi imbunatatita prin rularea unor thread-uri separate. In timp ce un thread este blocat in asteptarea unor informatii, altul poate sa isi continue executia.

Schimbarea de context in cazul thread-urilor este mai putin costisitoare. Thread-urile cer mai putine resurse si devine o solutie practica rularea de programe multithreaded pe sisteme uniprocesor. Totusi pe unele sisteme, cum este Linux, aceasta solutie nu este atat de eficienta pe cat pare. Linux-ul trateaza procesele si thread-urile in mod identic din punct de vedere al planificarii si executiei.

Tipuri de thread-uri

Sunt doua tipuri de thread-uri la nivelul unui sistem de operare: thread-uri la nivel utilizator (user-threads) si thread-uri la nivel kernel (kernel-threads). In cazul thread-urilor la nivel utilizator, planificarea si schimbarea de context se realizeaza in cadrul unei biblioteci specializate (fara interventia sistemului de operare - a kernel-ului). Thread-urile la nivelul kernel-ului sunt planificate exclusiv de catre sistemul de operare. Pentru crearea unui thread la nivel de kernel in Linux se utilizeaza apelul clone.

Tipuri de thread-uri la nivel utilizator sunt thread-urile POSIX (au fost standardizate in 1995) despre care vom discuta in continuare.

Thread-uri POSIX

Pentru utilizarea thread-urilor POSIX aveti nevoie de pachetul libpthread. Exista mai multe apeluri de biblioteca asociate, majoritatea incepand cu pthread_. Pentru utilizarea thread-urilor POSIX va trebui sa legati programul vostru cu biblioteca libpthread si sa includeti in fisierul sursa header-ul pthread.h.

Un exemplu de compilare si legare a unui program ce utilizeaza thread-uri POSIX este:

        gcc -Wall -o ptest ptest.c -lpthread

Este de asemenea util sa se defineasca macrodefinitia _REENTRANT in cadrul fisierelor sursa ce folosesc thread-uri POSIX. Acest lucru este necesar deoarece rutinele din biblioteca de C standard au fost gandite pentru procese cu un singur thread de executie. Spre exemplu variabila errno retine informatii de eroare despre ultimul apel executat. Intr-un mediu multithreaded accesul la variabila poate produce inconsistente.

De accea avem nevoie de rutine reentrante. In aceasta situatie codul reentrant poate fi apelat de mai multe ori fie din mai multe thread-uri, fie printr-un apel de tip recursiv si sa functioneze corect. De aceea sectiunea reentranta trebuie sa utilizeze numai variabile locale astfel incat fiecare apel catre aceasta secitiune va utiliza bucati de date unice.

Crearea unui thread

Pentru crearea unui thread in contextul procesului curent utilizam apelul pthread_create:

#include <pthread.h>

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

Desi pare destul de complex, apelul este unul usor de inteles si utilizat. Primul argument este un pointer la o data de tip pthread_t. Acesta este chiar adresa identificatorului thread-ului ce va fi creat. Fiecarui thread i se asociaza un identificator.

Al doilea argument stabileste atributele thread-ului. In mod obinsuit, aici se specifica NULL pentru a nu se utiliza atribute speciale. Vom discuta mai tarziu despre diverse tipuri de atribute care pot fi stabilite pentru a altera comportamentul unui thread.

Al treilea argument este un pointer de functie care primeste ca parametru un pointer generic si intoarce tot un pointer generic. Daca utilizarea apelului fork rezulta in procesul tata si procesul fiu sa reia executia din acelasi punct, un thread isi incepe executia cu functia primita ca parametru la creare.

Cel de-al patrulea parametru este argumentul care va fi transmis functiei pe care o executa codul.

Spre deosebire de majoritatea comenzilor UNIX care intorc 0 in caz de succes si -1 sau un alt numar negativ in caz de eroare, comenzile specifici thread-urilor POSIX intorc 0 in caz de succes si un alt numar (posibil pozitiv) pentru a raporta eroare. Din aceasta cauza identificatorul thread-ului este transmis ca parametru functiei de creare.

Terminarea unui thread

Terminarea unui thread poate fi realizata folosind apelul pthread_exit:

#include <pthread.h>

void pthread_exit (void *retval);

Apelul termina thread-ul curent si intoarce un pointer la un obiect. Nu trebuie intors un pointer la o variabila locala deoarece aceasta va disparea o data cu distrugerea thread-ului.

pthread_exit este apelat automat la atingerea sfarsitului functiei start_routine primita ca parametru de pthread_create. Parametru acesteia va fi, in acest caz, valoarea intoarsa de functie.

In momentul in care se apeleaza pthread_exit vor fi apelate consecutiv rutinele de cleanup asociate cu thread-ul curent. Oricarui thread ii pot fi asociate una sau mai multe rutine de cleanup cu rolul de eliberare a resurselor utilizate. Aceste rutine de cleanup au o structura de stiva. Apelurile specifice sunt:

#include <pthread.h>

void pthread_cleanup_push (void (*routine) (void *), void *arg);
void pthread_cleanup_pop (int execute);

Apelul pthread_cleanup_push adauga o noua rutina de cleanup.

Apelul pthread_cleanup_pop extrage cea mai recent instalata rutina de cleanup (intr-o maniera LIFO); daca argumentul execute este diferit de 0, se si executa aceasta rutina.

Asteptarea unui thread

Echivalentul apelului wait de asteptare a unui thread de catre thread-ul principal este apelul pthread_join:

#include <pthread.h>

int pthread_join (pthread_t th, void **thread_return);

Efectul apelului este suspendarea din executie a thread-ului curent pana in momentul in care thread-ul cu identificatorul th si-a incheiat executia. Daca thread_return nu este NULL atunci valoarea de retur a thread-ului asteptat este stocata la adresa indicata de thread_return.

In continuare este prezentat un program simplu care creeaza un thread suplimentar si arata cum se partajeaza informatiile in context multithreaded.

/*
 * simple_thread.c: exemplu simplu de prezentare a functionarii
 *     thread-urilor
 */

#define _REENTRANT1

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

void *thread_function (void *arg);

char message[] = "Hello World";

int main (void)
{
        int res;
        pthread_t a_thread;
        void *thread_result;

        res = pthread_create (&a_thread, NULL,
                              thread_function, (void *) message);
        if (res != 0) {
                perror ("Thread creation failed");
                exit (EXIT_FAILURE);
        }

        printf ("Waiting for thread to finish...\n");
        res = pthread_join (a_thread, &thread_result);
        if (res != 0) {
                perror("Thread join failed");
                exit(EXIT_FAILURE);
        }

        printf ("Thread joined, it returned %s\n", (char *) thread_result);
        printf ("Message is now %s\n", message);

        return 0;
}

void *thread_function (void *arg)
{
        printf ("thread_function is running. Argument was %s\n", (char *)arg);
        sleep (3);

        strcpy (message, "Bye!");
        pthread_exit ("Thank you for the CPU time");
}

Compilarea si rularea programului decurg in felul urmator:

razvan@ragnarok:~/cfiles/solab/labs/lab9$ gcc -Wall -o simple simple_thread.c -lpthread
razvan@ragnarok:~/cfiles/solab/labs/lab9$ ./simple
Waiting for thread to finish...
thread_function is running. Argument was Hello World
Thread joined, it returned Thank you for the CPU time
Message is now Bye!
razvan@ragnarok:~/cfiles/solab/labs/lab9$

Fortarea incheierii unui thread

Daca dorim ca un thread sa forteze terminarea unui alt thread, putem folosi apelul pthread_cancel:

#include <pthread.h>

int pthread_cancel (pthread_t thread);

Acest apel permite crearea unei cereri de incheiere a unui thread din cadrul altui thread. Ca si actiuni posibile, thread-ul care a primit cererea poate sa se termine imediat, sa ignore cererea sau sa amane terminarea. Stabilirea acestei actiuni poate fi realizata prin intermediul apelurilor pthread_setcanceltype si pthread_setcancelstate.

Atributele unui thread

S-a precizat ca la crearea unui thread se pot specifica o serie de atribute ca parametru, dar de obicei acel parametru va fi NULL. Pentru configuratii speciale, folosirea atributelor poate sa ajute.

Crearea unui atribut/set de atribute pentru un thread se realizeaza folosind comanda pthread_attr_init:

#include <pthread.h>

int pthread_attr_init (pthread_attr_t *attr);

Stabilirea diverselor atribute se realizeaza folosind apelurile din gama pthread_attr_set* iar culegerea informatiilor despre acele atribute se realizeaza folosind apelurile din gama pthread_attr_get*:

int pthread_attr_setdetachstate (pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate (const pthread_attr_t *attr, int *detachstate);
int pthread_attr_setschedpolicy (pthread_attr_t *attr, int policy);
int pthread_attr_getschedpolicy (const pthread_attr_t *attr, int *policy);
int pthread_attr_setstacksize (pthread_attr_t *attr, int scope);
int pthread_attr_getstacksize (pthread_attr_t *attr, int *scope);

Aceste atribute sunt in numar destul de mare si pot fi folosite pentru fine-tuning-ul anumitor aplicatii.

Atributul detachstate permite detasarea unui thread, in sensul ca nu se mai putea astepta terminarea lui folosind apelul pthread_join. Acest lucru este util in cazul in care desemnam un thread sa se ocupe de o anumita problema fara sa fie nevoie sa ne sincronizam cu terminarea lui folosind pthread_join.

Daca nu se specifica acest lucru la crearea thread-ului, noi putem sa detasam un thread folosind apelul pthread_detach:

int pthread_detach (pthread_t thread);

Apelul pthread_self poate fi folosit pentru a determina identificatorul thread-ului curent (asemanator getpid).

Exemplu

Un exemplu ceva mai complet de rulare a unui thread este urmatorul:

/*
 * search_thread.c: program de cautare multithreaded a unui element
 *     intr-un vector
 */

#define _REENTRANT1

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

#define NUM_THREADS10
#define ARRAY_SIZE100

#define min(a, b)((a) > (b) ? (b) : (a))

static int val;/* valoarea de cautat */
static int pos = -1;/* pozitia pe care a fost gasita valoarea */
static int array[ARRAY_SIZE];/* vectorul folosit la cautare */

struct thread_info {
        int start;
        int len;
};

static void *search (void *arg)
{
        struct thread_info *ti = (struct thread_info *) arg;
        int i;

        for (i = ti->start; i < ti->start + ti->len; i++) {
                if (array[i] == val) {
                        pos = i;
                        break;
                }
        }

        return NULL;
}

int main (int argc, char **argv)
{
        struct thread_info *ti;
        pthread_t threads[NUM_THREADS];
        int i;
        char *endp;

        if (argc != 2) {
                fprintf (stderr, "Usage: %s position\n", argv[0]);
                exit (EXIT_FAILURE);
        }

        val = strtol (argv[1], &endp, 10);
        if (*endp != '\0' && *endp != '\n') {
                fprintf (stderr, "Usage: %s position\n", argv[0]);
                exit (EXIT_FAILURE);
        }

        /* initializare naiva a vectorului */
        for (i = 0; i < ARRAY_SIZE; i++)
                array[i] = i;

        for (i = 0; i < NUM_THREADS; i++) {
                ti = (struct thread_info *) malloc (sizeof (*ti));
                if (ti == NULL) {
                        perror ("malloc");
                        exit (EXIT_FAILURE);
                }

                ti->start = i * (ARRAY_SIZE / NUM_THREADS + 1);
                ti->len = min (ARRAY_SIZE / NUM_THREADS + 1, ARRAY_SIZE - ti->start);

                if (pthread_create (&threads[i], NULL, search, ti) != 0) {
                        perror ("pthread_create");
                        exit (EXIT_FAILURE);
                }
        }

        for (i = 0; i < NUM_THREADS; i++) {
                if (pthread_join (threads[i], NULL) != 0) {
                        perror ("pthread_join");
                        exit (EXIT_FAILURE);
                }
        }

        if (pos == -1)
                printf ("Valoarea %d nu a fost gasita.\n", val);
        else
                printf ("Valoarea %d a fost gasita pe pozitia %d.\n", val, pos);

        return 0;
}

Rularea acestui program duce la urmatorul output:

razvan@ragnarok:~/cfiles/solab/labs/lab9$ gcc -Wall -o search search_thread.c -lpthread
razvan@ragnarok:~/cfiles/solab/labs/lab9$ ./search 4 Valoarea 4 a fost gasita pe pozitia 4.
razvan@ragnarok:~/cfiles/solab/labs/lab9$ ./search 7
Valoarea 7 a fost gasita pe pozitia 7.
razvan@ragnarok:~/cfiles/solab/labs/lab9$

About this document ...

Thread-uri. Thread-uri POSIX (pthreads)

This document was generated using the LaTeX2HTML translator Version 2002-2-1 (1.71)

Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999, Ross Moore, Mathematics Department, Macquarie University, Sydney.

The command line arguments were:
latex2html -no_subdir -split 0 -show_section_numbers /tmp/lyx_tmpdir4824JUxKiA/lyx_tmpbuf5/lab9.tex

The translation was initiated by Razvan Adrian Deaconescu on 2005-12-01


next_inactive up previous
Razvan Adrian Deaconescu 2005-12-01