LABORATIORIO DE ARQUITECTURA DE COMPUTADORES

Anuncio
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
Descargar