LABORATIORIO DE ARQUITECTURA DE COMPUTADORES INGENIERIA INFORMATICA PRACTICA 5 PROGRAMACION PARALELA (SISTEMAS SMP) Eduardo Moriana Delgado 2007/08 Introducción En esta práctica vamos a implementar un problema típico de cálculo numérico que consiste en hallar el numero pi, gracias al cálculo de la integral de la función 1 / 1 + x² en el intervalo [0,1] que según dicen es igual a pi/4. Gracias a esta ecuación seremos capaces de hallar el numero pi con cierta exactitud por supuesto siempre limitada a la precisión específica de los tipos de datos usados en el ordenador. La complejidad del ejercicio no se encuentra simplemente en la implementación de este programa en el lenguaje c, sino que se han de utilizar las librerías de Threads para aprovechar al máximo los computadores del tipo SMP. Implementación LA implementación y las pruebas se van a realizar sobre un sistema operativo Ubuntu 7.10, que corre sobre una maquina Intel Core 2 Duo T7100 a 1.80 Ghz y 2Gb de Ram. Para el desarrollo del código se ha utilizado el IDE Eclipse en su versión para c/c++. La implementación ha resultado menos compleja de los esperado pues mis conocimientos y experiencia en c son limitados, pero utilizando el ejemplo del tema en cuestión que aparece en las transparencias ha servido de base para la construcción de la práctica, y gracias al ejemplo del tema siguiente que facilita casi todo el código necesario para realizar la aproximación a la integral, el tiempo de “overhead” utilizado en cuestiones propias del lenguaje y no de los conceptos que son objetivo de la práctica, ha sido mínimizado. La implementación realizada difiere del ejemplo en que cada hilo accede a la variable global donde se almacenará el resultado total, y suma su parte cuando ha terminado de calcular la sección de la integral asignada, por lo tanto no existe un “capataz” sino que todos son autónomos y saben lo que tienen que hacer gracias a la naturaleza del ejercicio, donde únicamente con el identificador de proceso, son capaces de iniciar su trabajo. Las dudas han surgido en si era necesario el uso de la función de cancelación de los demás hilos cuando el último hubiera acabado, y finalmente se ha determinado el no utilizarlo, pues si vamos a realizar la cancelación por si acaso algún hilo se quedara bloqueado por algún motivo, y el criterio empleado es que todos hayan finalizado, sufriría un abrazo mortal. Entonces si se quisiera establecer un mecanismo de seguridad debería de ser basado en otro criterio como el tiempo o el uso de algún mutex que indique el buen desarrollo del proceso, algo parecido a un flag. Código /* Ejemplo de búsqueda de números primos */ #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <time.h> /* Constantes usadas */ #define workers 1 /* Hebras que realizan la búsqueda */ /* * Macros */ #define check(status,string) if (status != 0) { \ errno = status; \ fprintf(stderr, "%s status %d: %s\n", \ string, status, strerror(status)); \ } /* Datos globales */ pthread_mutex_t resultado_integral= PTHREAD_MUTEX_INITIALIZER; /* Mutex para el primo */ pthread_mutex_t current_mutex= PTHREAD_MUTEX_INITIALIZER; /* Número actual */ pthread_mutex_t cond_mutex= PTHREAD_MUTEX_INITIALIZER; /* Mutex para arranque */ pthread_cond_t cond_var= PTHREAD_COND_INITIALIZER; /* Variable de estado para el arranque */ float total = 0.0; /* Siguiente número a comprobar */ int thread_hold = 1; /* Número asociado al estado */ int count_endeds = 1; /* Cuenta de números de procesos acabaos */ pthread_t threads[workers]; /* Matriz con las hebras trabajadoras */ static void unlock_cond(void *arg) { int status; status = pthread_mutex_unlock(&cond_mutex); check(status, "Mutex_unlock"); } /* Prototipo de la función que integra */ double Trap(double local_a, double local_b, int local_n, double h); /* Función que vamos a integrar. Metemos el código que corresponda a la función que deseamos integrar */ double f(double x) { double return_val; return_val = 1 / (1 + x*x); return return_val; } /* Rutina de trabajo. Cada hebra arranca con esta rutina. Se realiza primero una espera diseñada para sincronizar los trabajadores con el capataz. Cada trabajador hace después su turno tomando un número del que determina si es primo o no. */ void *prime_search(void *arg) { int mi_rango; /* El rango de mi proceso */ double a = 0.0; /* Extremo izquierdo double b = 1.0; /* Extremo derecho int n = 1000000000; /* Número de trapecios */ double h; /* Base de cada trapecio */ double local_a; /* Extremo izdo. de mi proceso double local_b; /* Extermo dcho. de mi proceso int local_n; /* Número de trapecios para mi cálculo */ */ */ */ double integral; /* Resultado de la integral en mi intervalo */ mi_rango = (int)arg; int notifiee; /* Usada durante la cancelación */ int status; /* Status de las llamadas a pthread_* */ int not_done = 1; /* Predicado del lazo de trabajo */ int oldstate; /* Estado de cancelado previo */ /* Sincronizamos los trabajadores y el capataz usando una variable de estado cuyo predicado (thread_hold) será rellenado por el capataz. */ status = pthread_mutex_lock(&cond_mutex); check(status, "Mutex_lock"); pthread_cleanup_push(unlock_cond, NULL); while (thread_hold) { status = pthread_cond_wait(&cond_var, &cond_mutex); check(status, "Cond_wait"); } pthread_cleanup_pop(1); /* Realiza las comprobaciones sobre números cada vez mayores hasta encontrar el número deseado de primos. */ pthread_testcancel(); /* Obtener siguiente número a comprobar */ h = (b-a)/n; /* h es el mismo para todos los procesos */ local_n = n/workers; /* igual que el número de trapecios */ /* La longitud del intervalo de integración de cada proceso es igual a local_n*h. Así pues, mi intervalo empieza y acaba en: */ local_a = a + mi_rango*local_n*h; local_b = local_a + local_n*h; /* Calculo la integral en mi intervalo */ printf("trapecios = %d,mi numero %d, ", n, mi_rango); integral = Trap(local_a, local_b, local_n, h); printf("desde %f a %f = %f\n", local_a, local_b, total); /* Comprobamos la divisibilidad */ /* Inhibir posibles cancelaciones */ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate); /* */ Obtener mutex y añadir este primo a la lista. Cancelar el resto de hebras si ya se ha obtenido la cantidad pedida de primos. */ status = pthread_mutex_lock(&resultado_integral); check(status, "Mutex_lock"); total += integral; count_endeds++; // // // // // // // // if (count_endeds == workers) { for (notifiee = 0; notifiee < workers; notifiee++) { if (notifiee != mi_rango) { status = pthread_cancel(threads[notifiee]); check(status, "Cancel"); } } } status = pthread_mutex_unlock(&resultado_integral); check (status, "Mutex_unlock"); /* Permitir de nuevo cancelaciones */ pthread_setcancelstate(oldstate, &oldstate); pthread_testcancel(); return arg; } double Trap(double local_a, double local_b, int local_n, double h) { double integral; /* Para almacenar el resultado double x; int i; double f(double x); /* Función a integrar */ integral = (f(local_a) + f(local_b))/2.0; x = local_a; for (i = 1; i <= local_n-1; i++) { x = x + h; integral = integral + f(x); } integral = integral*h; return integral; } int main() { int worker_num; /* Índice de trabajadores */ void *exit_value; /* Estado final para cada trabajador */ int status; /* Status de las llamadas a pthread_* */ time_t start, end; double dif; int starti; int time_taken_millis1, time_taken_millis2; ; // do some stuff /* Creación de las hebras trabajadoras. */ for (worker_num = 0; worker_num < workers; worker_num++) { status = pthread_create(&threads[worker_num], NULL, prime_search, (void *)worker_num); check(status, "Pthread_create"); } /* Poner a cero el predicado thread_hold y señalizar globalmente que los trabajadores pueden comenzar. */ time(&start); starti = clock(); time_taken_millis1 = (int)((clock()-starti)*1E3/CLOCKS_PER_SEC); status = pthread_mutex_lock(&cond_mutex); check(status, "Mutex_lock"); thread_hold = 0; status = pthread_cond_broadcast(&cond_var); check(status, "Cond_broadcast"); status = pthread_mutex_unlock(&cond_mutex); check(status, "Mutex_unlock"); */ /* Hacer JOIN con cada trabajador para obtener los resultados y asegurarse de que todos se han completado correctamente. */ for (worker_num = 0; worker_num < workers; worker_num++) { status = pthread_join(threads[worker_num], &exit_value); check(status, "Pthread_join"); /* Si la terminación es correcta, el valor final exit_value es igual a worker_num. */ if (exit_value == (void *)worker_num) printf("Hebra %d terminada normalmente.\n", worker_num); else if (exit_value == PTHREAD_CANCELED) printf("Hebra %d fue cancelada.\n", worker_num); else printf("Hebra %d terminada con error %#lx.\n", worker_num, exit_value); } time(&end); /* Tomamos la lista de primos encontrados ordenamos de menor a mayor. Puesto que hay ninguna garantía respecto al orden Por tanto, es necesaria la ordenación. Algoritmo de la burbuja: ¡lo siento! */ por las hebras trabajadoras y los las hebras han trabajado en paralelo no en que están almacenados los primos. printf("FINALMENTE EL TOTAL ES %3.20f\n con tiempo = %ld\n",total*4); dif = difftime(end, start); printf("%.48lf Segundos para realizar el cálculo.\n", dif); printf("\n"); return 0; } Conclusiones y pruebas Para determinar los beneficios en toda su magnitud de este tipo de programación, se ncesita usar un computador del tipo SMP. Para las pruebas se va a utilizar el ordenador antes comentado, un Intel Core 2 Duo, y aunque nos sea un SMP puro sino una aproximación, los resultados obtenidos indican los beneficios de este paradigma de programación. Después de realizar varias pruebas, con diferente número de hilos y numero total de trapecios, se ha determinado que el unbral de número de trapecios a partir del que se empiezan a notar el speedup es de 10^8, donde con un hilo obtenemos 2 segundos aproxiamadamente y con dos hilos obtenemos 1 segundo. Si aumentamos el numero de hilos, el tiempo utilizado para el cálculo sigue siendo el mismo. Cuando aumentamos el numero de trapecios a 10^9, la diferencia se hace mucho mas cuantiosa, pues cuando probamos con unico hilo, obtenemos un tiempo de 20 segundos para obtener el cálculo, pero si utilizamos dos hilos, este tiempo se ve dismunuido a la mitad aproximadamente, 10 segudos. Igual que en la prueba anterior, el aumento del numero de hilos no es determinante pues como se explica en los apuntes, el numero de procesadores suele ser un buen determinante de la ganancia asociada al numero de hilos utilziados. A continuación se añade una imagen del monitor de los microprocesador, que indica su uso en el primer caso con un hilo, y en el segundo caso con dos. La imagen es muy ilustrativa y se puede observar en la carga de los procesadores, como en el primer caso solo se aprovecha la carga en uno, y como en el segundo caso, ambos procesadores utilizan toda su capacidad en el cálculo de la integral. El resultado obtenido con magnitud 10^9 es 3.14159274101257324219