1) Basic information - Thread operations include thread creation, termination, synchronization (joins,blocking), scheduling, data management and process interaction. - A thread does not maintain a list of created threads, nor does it know the thread that created it. - All threads within a process share the same address space. - Threads in the same process share: - Process instructions - Most data - open files (descriptors) - signals and signal handlers - current working directory - User and group id - Each thread has a unique: - Thread ID - set of registers, stack pointer - stack for local variables, return addresses - signal mask - priority - Return value: errno - pthread functions return "0" if OK. 2) Posix threads APIs and compilation - compile: gcc -pthread ... (or gcc -lpthread ...) - pthread_create - create a new thread - pthread_join - wait for termination of another thread - pthread_exit - terminate the calling thread - pthread_self - return identifier of current thread - pthread_mutex_init - init operations on mutexes - pthread_mutex_destroy - destroy a mutex - pthread_mutex_lock - acquires / locks a mutex; block until mutex is acquired - pthread_mutex_trylock - try to acquire / lock a mutex; does not block if mutex cannot be acquired - pthread_mutex_unlock - releases / unlocks a mutex previously acquired / locked 3) Thread synchronization - mutexes - Mutual exclusion lock: - Block access to variables by other threads. - This enforces exclusive access by a thread to a variable or set of variables. - used to prevent data inconsistencies due to operations by multiple threads upon the same memory area performed at the same time - used to prevent race conditions where an order of operation upon the memory is expected - joins - Make a thread wait till others are complete (terminated). - performed when one wants to wait for a thread to finish to get the results - condition variables - data type pthread_cond_t - used with the appropriate functions for waiting and later, process continuation - allows threads to suspend execution and relinquish the processor until some condition is true - A condition variable must always be associated with a mutex to avoid a race condition created by one thread preparing to wait and another thread which may signal the condition before the first thread actually waits on it resulting in a deadlock 4) Examples - create and terminate: #include <stdio.h> #include <stdlib.h> #include <pthread.h> void *print_message_function( void *ptr ); main() { pthread_t thread1, thread2; const char *message1 = "Thread 1"; const char *message2 = "Thread 2"; int iret1, iret2; /* Create independent threads each of which will execute function */ iret1 = pthread_create( &thread1, NULL, print_message_function, (void*) message1); if(iret1) { fprintf(stderr,"Error - pthread_create() return code: %d\n",iret1); exit(EXIT_FAILURE); } iret2 = pthread_create( &thread2, NULL, print_message_function, (void*) message2); if(iret2) { fprintf(stderr,"Error - pthread_create() return code: %d\n",iret2); exit(EXIT_FAILURE); } printf("pthread_create() for thread 1 returns: %d\n",iret1); printf("pthread_create() for thread 2 returns: %d\n",iret2); /* Wait till threads are complete before main continues. Unless we */ /* wait we run the risk of executing an exit which will terminate */ /* the process and all threads before the threads have completed. */ pthread_join( thread1, NULL); pthread_join( thread2, NULL); exit(EXIT_SUCCESS); } void *print_message_function( void *ptr ) { char *message; message = (char *) ptr; printf("%s \n", message); } - synchronization: #include <stdio.h> #include <pthread.h> #define NTHREADS 10 void *thread_function(void *); pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER; int counter = 0; main() { pthread_t thread_id[NTHREADS]; int i, j; for(i=0; i < NTHREADS; i++) { pthread_create( &thread_id[i], NULL, thread_function, NULL ); } for(j=0; j < NTHREADS; j++) { pthread_join( thread_id[j], NULL); } /* Now that all threads are complete I can print the final result. */ /* Without the join I could be printing a value before all the threads */ /* have been completed. */ printf("Final counter value: %d\n", counter); } void *thread_function(void *dummyPtr) { printf("Thread number %ld\n", pthread_self()); pthread_mutex_lock( &mutex1 ); counter++; pthread_mutex_unlock( &mutex1 ); } 5) Exercitiu laborator A) Sa se afiseze cate numere prime sunt in intervalul 1-2,000,000,000. - in prima varianta se implementeaza iterativ - se masoara timpul de executie - se incearca cu 2, 4, 8 threaduri, fiecare prelucrand un subinterval, si se va determina care varianta este mai rapida Note: Pentru masurarea timpul de executie se poate folosi o secventa similara cu: time_t t1 = time(NULL); // perform complex and time consuming stuff time_t t2 = time(NULL); double x = difftime(t2, t1); printf("%.0f", x); B) Sa se afiseze cate numere prime sunt in intervalul 1-2,000,000,000. - threadul principal va crea n threaduri (n = 2, 4, 6, 8) - fiecare thread va incrementa o variabila globala care reprezinta numarul curent care trebuie verificat daca este prim si il va verifica - fiecare thread va incrementa o variabila globala care reprezinta cate numere prime au fost identificate daca la pasul anterior a determinat ca numarul este prim - threadul principal va astepta ca toate cele n threaduri sa isi incheie executia - threadul principal va afisa cate numere prime au fost determinate - prima implementare va fi realizata fara sincronizare - a doua varianta va fi realizata cu sincronizare folosind mutex - a treia varianta va fi realizata folosind operatii interlocked - fiecare varianta va contine cod de masurare a timpului de executie Note: Pentru masurarea timpul de executie se poate folosi o secventa similara cu: time_t t1 = time(NULL); // perform complex and time consuming stuff time_t t2 = time(NULL); double x = difftime(t2, t1); printf("%.0f", x); 6) Practice Sa se scrie un program c care citeste un nume de fisier de la tastatura si un caracter (cheie de criptare) tot de la tastatura. Programul va cripta continutul fisierului introdus de la tastatura folosind ca si cheie de criptare caracterul introdus de utilizator. Rezultatul criptarii va fi scris intr-un fisier cu acelasi nume ca si fisierul initial la care se adauga extensia: .crypt. Criptarea se va realiza conform urmatorului algoritm: crypt = original XOR cheie. (Partea 1) Threadul principal va citi numele fisierului si cheia de criptare de la tastatura, va crea un thread caruia ii va transmite ca si parametru numele si cheia citite de la tastaura Threadul creat va deschide fisierul primit ca parametru, va citi continutul, va cripta continutul si il va scrie in fisierul destinatie Threadul principal va astepta pana cand procesarea in thread se va termina si va afisa pe ecran rezultatul criptarii. (Partea 2) Threadul principal va citi numele fisierului si cheia de criptare de la tastatura, va crea cate un thread pt fiecare chunk (bucata) de 1024kb a fisierului initial caruia ii va transmite ca si parametru numele si cheia citite de la tastaura precum si index-ul chunk-ului asignat Threadul creat va deschide fisierul primit ca parametru, va citi continutul chunk-ului primit din fisier, va cripta continutul si il va scrie in fisierul destinatie Threadul principal va astepta pana cand procesarea in thread se va termina si va afisa pe ecran rezultatul criptarii. (Partea 3) Car bridge under reparation. There is a bridge over a river, that is under reparation. Use locks and condition variables to synchronize, in a C program, the threads simulating cars, such that the following conditions hold simultaneously: 1. Cars can pass the bridge in only one direction at one moment. The crossing direction is changed periodically (for instance, every 5 seconds) by another thread traffic_controller. 2. The maximum number of cars on the bridge is limited to MAX. 3. When the crossing direction is changed, the cars that are just given the permission to enter the bridge must wait until all the cars from the opposite direction exit the bridge. 4. NOTE 1. The threads playing roles of cars execute the following function: struct car_args { int car_id; int car_direction; } void* car(struct car_args *arg) { enter_bridge(arg->car_id, arg->car_direction); pass_bridge(arg->car_id, arg->car_direction); exit_bridge(arg->car_id, arg->car_direction); return NULL; } 5. NOTE 2. The thread controlling the passing direction executes the following function: int passing_direction = 0; void* trafic_controller(void* arg) { while (1) { sleep(5); change_passing_direction(); } } (Partea 4 - pregatire pentru examen) Sa se scrie un program care genereaza pe 5 thread-uri distincte 1000 de numere intregi pe care le introduce intr-un vector global. Fiecare numar generat va fi introdus de thread-ul care l-a generat pe pozitia corecta in vector, astfel incat la orice moment vectorul sa fie sortat. Threadul principal va afisa la fiecare 10 numere generate intreg continutul vectorului. (Partea 5 - pregatire pentru examen) Sa se scrie un program care genereaza pe 5 thread-uri distincte 1000 de numere intregi pe care le introduce intr-un vector global. Fiecare numar generat va fi introdus de thread-ul care l-a generat pe pozitia corecta in vector, astfel incat la orice moment vectorul sa fie sortat. Programul va creea alte 3 thread-uri care extrag din vector numarul cel mai apropiat de media aritmetica a numerelor prezente in vector. Extragerea va avea loc la fiecare 10 numere generate. Threadul principal va afisa la fiecare 10 numere generate intreg continutul vectorului.