Departamento de Automática Arquitectura e Ingeniería de Computadores Tema 6 Programación paralela de sistemas SMP Prof. Dr. José Antonio de Frutos Redondo Dr. Raúl Durán Díaz Curso 2010-2011 Arquitectura e Ingeniería de Computadores Programación de sistemas SMP Introducción. Hebras. Sincronización. Visibilidad de la memoria entre hebras. Paradigmas de uso. Programación avanzada. Errores frecuentes. Programación de un ejemplo. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 2 V1.3 1 Arquitectura e Ingeniería de Computadores Introducción Definición y terminología. Concepto de hebra. Comprende los elementos necesarios para ejecutar una sucesión de instrucciones máquina: Concepto de proceso (en Unix): Contador de programa, los registros de datos y direcciones, etc. Una o más hebras, espacio de direcciones, descriptores de ficheros, etc. Por lo tanto, un proceso puede estar ejecutando varias hebras que compartirán el espacio de direcciones. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 3 V1.3 Arquitectura e Ingeniería de Computadores Introducción Definición y terminología. Asincronía. Dos operaciones son asíncronas cuando evolucionan con total independencia una de otra. Concurrencia. Dos operaciones son concurrentes cuando se pueden entremezclar arbitrariamente de forma que ambas progresen independientemente. Es decir: no se necesita que una acabe para comenzar la otra. Paralelismo. Se aplica a dos operaciones concurrentes que, además, progresan simultáneamente. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 4 V1.3 2 Arquitectura e Ingeniería de Computadores Introducción Definición y terminología. La concurrencia puede darse tanto en un sistema monoprocesador como multiprocesador. El paralelismo sólo puede darse en un sistema multiprocesador. El modelo de hebras proporciona tanto concurrencia como paralelismo, lo que lo hace útil en ambos tipos de máquinas. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 5 V1.3 Arquitectura e Ingeniería de Computadores Introducción Definición y terminología. Código seguro en el modelo de hebra. Llamamos código seguro al que puede ser ejecutado simultáneamente por varias hebras sin resultados destructivos. Atención: no es lo mismo “seguro” que “eficiente”. Podemos convertir una función (que no tenga estado) en segura utilizando un bloqueo al llamarla que liberamos al terminar. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 6 V1.3 3 Arquitectura e Ingeniería de Computadores Introducción Definición y terminología. Hebra re-entrante. Se trata de un modelo “eficiente” de código seguro. Para ello: evitar datos estáticos (es decir, persistentes de una llamada a otra); independiente de cualquier forma de sincronización entre las hebras. Cuando es necesario guardar el estado, es mejor hacerlo sobre estructuras de datos externas a la función que puede controlar y sincronizar el llamador. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 7 V1.3 Arquitectura e Ingeniería de Computadores Introducción Definición y terminología. Protección de código frente a protección de datos. Conceptualmente es mejor proteger los datos que el código. Tomemos la función putchar como ejemplo: Protegemos su ejecución mediante un bloqueo (mutex): Ventaja: garantizamos que sólo una hebra puede escribir un carácter cada vez. Problema: si una hebra quiere escribir en otro canal, ¡no puede hacerlo! Solución: proteger el canal (es decir, su estructura de datos asociada) y no el código. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 8 V1.3 4 Arquitectura e Ingeniería de Computadores Introducción Costes de la programación multihebra. Sobrecarga de computación. La sincronización tiene coste computacional: hay que mantenerla al mínimo. Deficiencias a niveles más bajos. Si se usan hebras para paralelizar la I/O, pueden aparecer cuellos de botella en: Librería ANSI C. Sistema operativo. Sistema de ficheros. Drivers de los dispositivos. En procesos de computación intensiva, adecuar el número de hebras al de procesadores (fase de mapeo). Más hebras que procesadores suele dar peor rendimiento. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 9 V1.3 Arquitectura e Ingeniería de Computadores Introducción Costes de la programación multihebra. Autodisciplina del programador. La programación con hebras es más difícil. Hay que ser cautos con el código desarrollado por terceros. ¡No olvidar nunca que cualquier hebra ve todo el espacio de direcciones de las demás! Dificultad de depuración. Faltan herramientas. Los errores son sutiles: La temporización es clave y puede hacer aparecer o desaparecer errores. La corrupción de punteros es muy difícil de rastrear. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 10 V1.3 5 Arquitectura e Ingeniería de Computadores Introducción Normativa POSIX. Seguimos la norma POSIX 1003.1c – 1995. El nombre pthreads se refiere a POSIX threads. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 11 V1.3 Arquitectura e Ingeniería de Computadores Hebras Creación y uso. Creación y gestión de hebras. Objeto básico: pthread_t thread; Funciones relacionadas: int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start)(void *), void *arg); int pthread_exit(void *value_ptr); int pthread_detach(pthread_t thread); int pthread_join(pthread_t thread, void **value_ptr); int sched_yield(void); int pthread_equal(pthread_t t1, pthread_t t2); © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 12 V1.3 6 Arquitectura e Ingeniería de Computadores Hebras La hebra se representa con el tipo de dato pthread_t. La ejecución de la hebra comienza llamando a la función que se indique en start, a la que se le pasará el argumento arg. El identificador de la hebra creada se almacena en la variable tipo pthread_t. El identificador es necesario para actuar sobre la hebra. Una hebra puede obtener su propio identificador con pthread_self(). No hay forma de obtener el de otra, salvo que se haya guardado previamente. La función pthread_equal() permite comparar dos identificadores. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 13 V1.3 Arquitectura e Ingeniería de Computadores Hebras El main es la hebra inicial. Si se termina, se termina todo el proceso (en el sentido del sistema operativo): obliga a las demás hebras a terminar; devuelve al sistema operativo los recursos de todo el proceso. Por lo demás se comporta como una hebra normal. Todas las hebras creadas comparten el espacio de direcciones y los descriptores de ficheros. A cada hebra se le asigna una pila propia de tamaño fijo, especificable, mediante el parámetro de atributos, en el momento de creación. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 14 V1.3 7 Arquitectura e Ingeniería de Computadores Hebras La función pthread_exit() termina una hebra. Una hebra terminada sólo devuelve sus recursos al sistema operativo si está en estado detached. Para poner una hebra en estado detached se utiliza la función pthread_detach(). El estado detached no afecta a la hebra: sólo informa al sistema operativo de que sus recursos se pueden recuperar cuando ésta termine. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 15 V1.3 Arquitectura e Ingeniería de Computadores Hebras Ciclo de vida: desbloqueada Lista Bloqueada detenida creada planificada esperando recurso Corriendo terminada o cancelada Terminada © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 16 V1.3 8 Arquitectura e Ingeniería de Computadores Hebras Una hebra puede devolver un valor al terminar, si otra hebra llama a la función pthread_join(). Recordemos que la sintaxis es: int pthread_join(pthread_t thread, void **value_ptr); Si no nos interesa el valor retornado, podemos pasar NULL en lugar de value_ptr. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 17 V1.3 Arquitectura e Ingeniería de Computadores Relación entre hebras y kernel Entre las hebras y el procesador, suele haber un nivel de abstracción intermedio, como, por ejemplo: Un proceso Unix tradicional, Una hebra de kernel, Un light-weight process, como en Solaris, etc. Estas “entidades de kernel” están gestionadas por el sistema operativo. Se relaciona con las hebras a través de algún subsistema que llamamos planificador. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 18 V1.3 9 Arquitectura e Ingeniería de Computadores Relación entre hebras y kernel Las hebras se relacionan con las “entidades” del kernel como en esta figura: Hebras (creadas por el usuario) Planificador El usuario puede ejercer cierto control sobre la planificación. Entidades del kernel (gestionadas por el SO) © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 19 V1.3 Arquitectura e Ingeniería de Computadores Ejemplo de programación con fork Programación asíncrona. Ejemplo base: # include “todo.h” # define TRUE 1 int main(int argc, char *argv[]) { int segundos = 0; char line[128] = “”; char mensaje[64] = “”; while(TRUE) { printf(“Alarma> “); if (fgets(line, sizeof(line), stdin) == NULL) exit(0); if (strlen(line) <= 1) continue; if (sscanf(line, “%d %64[^\n]”, &segundos, mensaje) < 2) fprintf(stderr, “Comando erróneo.\n”); else { sleep(segundos); printf(“(%d) ‘%s’\n”, segundos, mensaje); } } exit(0); } © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 20 V1.3 10 Arquitectura e Ingeniería de Computadores Ejemplo de programación con fork Ejemplo de programación asíncrona creando procesos: ... else { pid = fork(); if (pid == 0) { sleep(segundos); printf(“(%d) ‘%s’\n”, segundos, mensaje); exit(0); } else { do { pid = waitpid(-1, NULL, WNOHANG); } while (pid != 0); } } } exit(0); } © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 21 V1.3 Arquitectura e Ingeniería de Computadores Ejemplo de programación con hebras Ejemplo de programación asíncrona usando hebras: # include <pthread.h> typedef struct { int segundos; char mensaje[64]; } alarm_t; ... alarm_t *alarm; pthread_t thread; ... alarm = (alarm_t *)malloc(sizeof(alarm_t)); ... else { pthread_create(&thread, NULL, alarm_thread, alarm); } } exit(0); } © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 22 V1.3 11 Arquitectura e Ingeniería de Computadores Ejemplo de programación con hebras Ejemplo de programación asíncrona usando hebras: # include <pthread.h> void *alarm_thread(void *arg) { alarm_t *alarm = (alarm_t *)arg; pthread_detach(pthread_self()); sleep(alarm->segundos); printf(“(%d) ‘%s’\n”, segundos, mensaje); free(alarm); return NULL; } © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 23 V1.3 Arquitectura e Ingeniería de Computadores Sincronización Con frecuencia existen unas relaciones implícitas entre diversas variables de un programa. Ejemplo: una cola de elementos con cabecera la cabecera apunta al primer elemento o es NULL. cada elemento apunta al siguiente o a NULL si es el último. Si estas relaciones no se respetan, el programa fallará o dará resultados erróneos. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 24 V1.3 12 Arquitectura e Ingeniería de Computadores Sincronización Secciones críticas. Las relaciones entre variables se pueden “infringir” temporalmente con tal de que el resto del código no pueda “enterarse” de ello. Áreas de código que afectan a datos compartidos. Pueden ser vistas también como invariantes de datos. Por ejemplo, el estado de la cola no debe alterarse cuando se inserta o se elimina un elemento. Cuando hablamos de sincronización nos referimos a la protección del programa frente a la alteración de las relaciones. La sincronización es un proceso cooperativo. Los predicados son expresiones lógicas que describen el estado de las relaciones. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 25 V1.3 Arquitectura e Ingeniería de Computadores Sincronización Mutexes (semáforos de exclusión mutua). Para sincronizar las hebras hay que asegurar el acceso mutuamente excluyente a los datos compartidos. La sincronización es importante para: Sólo una hebra accede cada vez. modificar datos; para leer datos previamente escritos si el orden es importante. ¡Atención! Con frecuencia, el hardware no nos garantiza un orden predecible en el acceso a memoria. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 26 V1.3 13 Arquitectura e Ingeniería de Computadores Sincronización Creación y destrucción de mutexes. Objeto básico: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; Funciones relacionadas: Creación: int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr); Esta función sirve para crear un mutex dinámicamente. Destrucción: int ptread_mutex_destroy(pthread_mutex_t *mutex); Un mutex creado dinámicamente se puede destruir una vez se tenga certeza de que no hay hebras que lo retengan bloqueado. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 27 V1.3 Arquitectura e Ingeniería de Computadores Sincronización Bloqueo y desbloqueo de mutexes: int pthread_mutex_lock(pthread_mutex_t *mutex); Esta función espera hasta conseguir el bloqueo del mutex. ¡Atención! No bloquear un mutex que la misma hebra ya tenía bloqueado previamente. int pthread_mutex_unlock(pthread_mutex_t *mutex); Una hebra sólo puede desbloquear los mutexes que le pertenecen, es decir, los que ella bloqueó. int pthread_mutex_trylock(pthread_mutex_t *mutex); Esta función devuelve el error EBUSY si el mutex ya estaba bloqueado. Puede servir para evitar los “abrazos mortales”. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 28 V1.3 14 Arquitectura e Ingeniería de Computadores MUTEX (MUTual EXclusion) Secuencia de operaciones con mutex Hebra 2 espera Hebra 1 bloquea Hebra 1 Hebra 3 espera Hebra 1 intenta bloquear Retorno EBUSY Hebra 2 Hebra 3 mutex tiempo Hebra 1 bloquea Hebra 1 desbloquea Hebra 2 bloquea © J. A. de Frutos Redondo, R. Durán 2006 Hebra 2 desbloquea 6. Programación paralela de sistemas SMP 29 V1.3 Arquitectura e Ingeniería de Computadores Sincronización Variables de estado. Se utilizan para comunicar el estado de los datos compartidos. Una variable de estado siempre lleva asociado un mutex. La hebra bloquea el mutex y después espera cambio de estado. El sistema pthreads asegura atomicidad en las operaciones espera cambio de estado y desbloqueo de mutex. Las variables de estado sirven para señalizar una condición: no proporcionan exclusión mutua. Ejemplo: la hebra que atiende una cola puede señalizar que ésta está vacía para que otra hebra actúe apropiadamente. Por eso se utilizan en conjunción con los mutexes. Cada variable de estado lleva asociado uno y solo un predicado. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 30 V1.3 15 Arquitectura e Ingeniería de Computadores Sincronización Creación y destrucción de variables de estado. Objeto básico: pthread_cond_t cond = PTHREAD_COND_INITIALIZER; Funciones relacionadas: Creación: int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr); Crea una variable de estado dinámicamente. Destrucción: int ptread_cond_destroy(pthread_cond_t *cond); Una variable de estado creada dinámicamente se puede destruir una vez se tenga certeza de que no hay ni habrá hebras a la espera de cambio de estado, ni ninguna señalizará ningún cambio de estado. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 31 V1.3 Arquitectura e Ingeniería de Computadores Sincronización Funciones de espera: Espera ordinaria: int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); Cada variable de estado debe estar asociada con uno y sólo un mutex. Cuando la hebra invoca esta función, ha de tener bloqueado el mutex. La operación de espera liberará el mutex justo al comenzar la espera. Una vez señalizada la condición, la función se desbloquea y retoma el bloqueo del mutex antes de seguir adelante. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 32 V1.3 16 Arquitectura e Ingeniería de Computadores Sincronización Funciones de espera: Espera con plazo de expiración: int ptread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, struct timespec *expiration); La función puede retornar bien por señalización de la condición bien por expiración del plazo. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 33 V1.3 Arquitectura e Ingeniería de Computadores Sincronización Puntos importantes: Chequear siempre el predicado. después de obtener el bloqueo del mutex y antes de llamar a la función de espera; al retornar de la función de espera. Lo mejor es esperar dentro de un lazo el cambio de estado. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 34 V1.3 17 Arquitectura e Ingeniería de Computadores Sincronización Señalización de cambio de estado: Señalizar a una sola hebra: int ptread_cond_signal(pthread_cond_t *cond); Necesitamos señalizar a sólo una hebra el cambio de estado. Cualquiera de las hebras en espera puede procesar ese cambio. Señalizar a todas las hebras simultáneamente: int ptread_cond_broadcast(pthread_cond_t *cond); No es imprescindible bloquear el mutex asociado a una variable de estado antes de señalizarla. Puede ser más eficiente en muchos sistemas. Pero si entre la señalización y el retorno otra hebra adquiere el mutex, la hebra señalizada ha de esperar que ésta lo libere. Puede darse así la inversión de prioridad. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 35 V1.3 Arquitectura e Ingeniería de Computadores VARIABLES DE CONDICIÓN Hebra 1 Hebra 2 Hebra 3 Variable de condición Señal de hebra 1 sin hebras esperando Hebra 1 espera Hebra 2 espera Señal de hebra 3 despierta hebra1 Hebra 3 espera © J. A. de Frutos Redondo, R. Durán 2006 Tiempo de espera de hebra 3 superado Hebra 3 espera con limite de tiempo Hebra 1 broadcast Despiertan 2 y 3 6. Programación paralela de sistemas SMP 36 V1.3 18 Arquitectura e Ingeniería de Computadores Visibilidad de la memoria entre hebras La norma implica seguir estas reglas: Una hebra recién creada ve en la memoria los mismos valores que la hebra que la creó. Una hebra que adquiere un mutex ve en la memoria los mismos valores que la hebra que previamente liberó el mutex. Una hebra que hace join con otra que termina ve en memoria los mismos valores que ésta. Una hebra que es señalizada ve en memoria los mismos valores que la hebra señalizadora. Cualquier dato escrito después de alguna de las acciones precedentes no se garantiza que sea visto por la hebra afectada. No se puede establecer supuestos respecto al orden en que los datos se escribirán en memoria. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 37 V1.3 Arquitectura e Ingeniería de Computadores Paradigmas de uso Existen muchas soluciones para estructurar una solución de tipo hebra. Los paradigmas básicos son: Modelo trabajo en cadena. Modelo grupo de trabajo. Modelo cliente-servidor. Combinación de modelos. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 38 V1.3 19 Arquitectura e Ingeniería de Computadores Paradigmas de uso Modelo trabajo en cadena. El trabajo se descompone en una serie de tareas secuenciales, con una entrada y una salida claramente definidas. Cada hebra realiza una o más de estas tareas secuenciales sobre conjuntos de datos sucesivos, pasando el resultado a otra hebra para realice el siguiente paso. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 39 V1.3 Arquitectura e Ingeniería de Computadores Paradigmas de uso Modelo trabajo en cadena: Entrada Hebra A Hebra B Hebra C Salida © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 40 V1.3 20 Arquitectura e Ingeniería de Computadores Paradigmas de uso Modelo grupo de trabajo. El trabajo se descompone en tareas paralelas. Se descompone un conjunto de datos en subconjuntos, cada uno de los cuales será procesado por una hebra. El concepto es en parte similar a una máquina tipo SIMD. Un grupo de trabajo es un conjunto de hebras que realizan procesamiento independiente sobre conjuntos distintos de datos. En este sentido se puede comparar más a un MIMD. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 41 V1.3 Arquitectura e Ingeniería de Computadores Paradigmas de uso Modelo grupo de trabajo: Entrada Hebra A Hebra B Hebra C Salida © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 42 V1.3 21 Arquitectura e Ingeniería de Computadores Paradigmas de uso Modelo cliente-servidor. El servidor (o servidores) realiza una determinada tarea especializada. El cliente pide al servidor la realización de ese trabajo y, generalmente, procede a realizar otras tareas, por ejemplo, solicitar otros trabajos a otros servidores. El cliente se limita así a coordinar las tareas entre distintos servidores. Por eso este modelo se conoce también como “capataz-trabajadores”. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 43 V1.3 Arquitectura e Ingeniería de Computadores Paradigmas de uso Modelo cliente-servidor: Entrada A Entrada B Entrada C Servidor Salida A © J. A. de Frutos Redondo, R. Durán 2006 Salida B Salida C 6. Programación paralela de sistemas SMP 44 V1.3 22 Arquitectura e Ingeniería de Computadores Paradigmas de uso Combinación de modelos. Los modelos se pueden combinar de cualquier modo imaginable, para adaptarse a las necesidades. Ejemplos: Una fase de un modelo trabajo en cadena se puede implementar usando cliente-servidor. Un servidor puede estar implementado usando trabajo en cadena. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 45 V1.3 Arquitectura e Ingeniería de Computadores Programación avanzada Inicialización única. Objetos atributo. Atributos de mutex. Atributos de variable de estado. Atributos de hebra. Cancelación. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 46 V1.3 23 Arquitectura e Ingeniería de Computadores Programación avanzada Inicialización única. Se utiliza cuando hay alguna o algunas operaciones que han de ser realizadas solamente una vez por no importa qué hebra. Ejemplos: inicialización de variables, creación de mutexes, creación de datos específicos de hebra, etc. Normalmente usaremos una variable booleana para controlar, protegida por un mutex inicializado estáticamente. En un programa estas operaciones se realizan típicamente en el main. Si esto no es posible, (por ejemplo, en una librería) usamos la función pthread_once. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 47 V1.3 Arquitectura e Ingeniería de Computadores Programación avanzada Objeto básico: pthread_once_t once_control = PTHREAD_ONCE_INIT; Funciones relacionadas: int pthread_once(pthread_once_t *once_control, void (*rutina_inicio)(void)); La función rutina_inicio será ejecutada sólo una vez por no importa qué hebra. La pthread_once comprueba primero la variable de control once_control, para determinar si se ha completado ya la inicialización. Al terminar, la función llamante tiene garantizada la inicialización. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 48 V1.3 24 Arquitectura e Ingeniería de Computadores Programación avanzada Objetos atributo. Un objeto atributo es una lista extendida de argumentos que se añade cuando se van a crear ciertos objetos. Permite que la creación de esos objetos sea “sencilla”, para el neófito, pero flexible para que un “experto” pueda sacar todo el partido. Se pueden aplicar a: Atributos de mutex. Atributos de variable de estado. Atributos de hebra. No todas las implementaciones soportan todos los atributos. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 49 V1.3 Arquitectura e Ingeniería de Computadores Programación avanzada Atributos de mutex. Objeto básico: pthread_mutexattr_t attr; Creación: int pthread_mutexattr_init(pthread_mutexattr_t *attr); Destrucción: int pthread_mutexattr_destroy(pthread_mutexattr_t *attr); Atributo soportado: compartición del mutex entre procesos. int pthread_mutexattr_getpshared(pthread_mutexattr_t *attr, int *pshared); int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared); © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 50 V1.3 25 Arquitectura e Ingeniería de Computadores Programación avanzada Atributos de variable de condición. Objeto básico: pthread_condattr_t attr; Creación: int pthread_condattr_init(pthread_condattr_t *attr); Destrucción: int pthread_condattr_destroy(pthread_condattr_t *attr); Atributo soportado: compartición de la variable de condición entre procesos. int pthread_condattr_getpshared(pthread_condattr_t *attr, int *pshared); int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared); © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 51 V1.3 Arquitectura e Ingeniería de Computadores Programación avanzada Atributos de hebra. Objeto básico: pthread_attr_t attr; Creación: int pthread_attr_init(pthread_attr_t *attr); Destrucción: int pthread_attr_destroy(pthread_attr_t *attr); © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 52 V1.3 26 Arquitectura e Ingeniería de Computadores Programación avanzada Atributos soportados para hebra. Detach state: Si el identificador de hebra puede usarse para hacer JOIN, se trata de una hebra JOINABLE. El valor del atributo será PTHREAD_CREATE_JOINABLE. Si cuando la hebra termina, puede devolver inmediatamente todos sus recursos al sistema operativo, la hebra es DETACHED. El valor del atributo será PTHREAD_CREATE_DETACHED. Por defecto las hebras se crean en modo JOINABLE. int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate); int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 53 V1.3 Arquitectura e Ingeniería de Computadores Programación avanzada Atributos soportados para hebra. Tamaño de pila: En algunos casos puede ser interesante modificar la cantidad de memoria reservada para la pila. Ejemplo: si la jerarquía de llamadas a funciones baja demasiados niveles. Atención: cambiar el tamaño de la pila hace el código poco portátil. int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize); int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 54 V1.3 27 Arquitectura e Ingeniería de Computadores Programación avanzada Cancelación. Las hebras suelen terminar por sí mismas, pero a veces es interesante cancelarlas. Ejemplos: Cancelar una hebra no la mata automáticamente: depende de su estado de cancelación. El usuario presiona el botón “Cancelar”. Una hebra es parte de un algoritmo redundante en que otra hebra ya ha obtenido resultados válidos. Es, más bien, pedirle de forma “educada” que termine. Necesitamos su identificación: int pthread_cancel(pthread_t thread); © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 55 V1.3 Arquitectura e Ingeniería de Computadores Programación avanzada Cancelación. Se codifica mediante dos bits: Bit de estado: Bit de tipo: enable disable deferred asynchronous Por defecto, la cancelación es deferred. Significa que sólo puede ocurrir en ciertos puntos del programa, en que la propia hebra comprueba si se le ha pedido terminar. Ejemplo: al esperar el cambio de una variable de condición, al leer o escribir un fichero, etc. Se puede pedir explícitamente la comprobación mediante pthread_testcancel(void); © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 56 V1.3 28 Arquitectura e Ingeniería de Computadores Programación avanzada Cancelación. En el tipo asíncrono indicamos que la hebra puede finalizar inmediatamente, sin ulteriores comprobaciones. Funciones de cambio de tipo y estado: int pthread_setcancelstate(int state, int *oldstate); Los valores de state pueden ser: PTHREAD_CANCEL_ENABLE PTHREAD_CANCEL_DISABLE int pthread_setcanceltype(int type, int *oldtype); Los valores de type pueden ser: PTHREAD_CANCEL_DEFERRED PTHREAD_CANCEL_ASYNCHRONOUS © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 57 V1.3 Arquitectura e Ingeniería de Computadores Programación avanzada Cancelación. Funciones para insertar una rutina de limpieza antes de que se ejecute la cancelación: int pthread_cleanup_push(void (*rutina)(void *), void *arg); Envía la rutina rutina a la pila de handlers de limpieza. Será ejecutada si: Si es cancelada la hebra. Si la hebra ejecuta pthread_exit Si la hebra ejecuta pthread_cleanup_pop con un valor de argumento distinto de cero. int pthread_cleanup_pop(int execute); Saca de la pila el handler introducido por una llamada a la pthread_cleanup_push. Además lo ejecuta si el argumento es distinto de cero. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 58 V1.3 29 Arquitectura e Ingeniería de Computadores Errores frecuentes “Inercia” de las hebras. Competición entre las hebras. Evitar abrazos mortales. Inversión de prioridad. Compartición de variables de estado en diferentes predicados. Compartición de variables privadas. Pila Problemas de rendimiento. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 59 V1.3 Arquitectura e Ingeniería de Computadores Errores frecuentes “Inercia” de las hebras. Recordar siempre que las hebras son asíncronas. En un sistema monoprocesador, una hebra que crea otra tiene una cierta ventaja sobre ella en cuanto al momento del comienzo de ejecución: hay un “ligero sincronismo”. También puede pasar lo mismo en un multiprocesador si se ha alcanzado el límite de procesadores disponibles. Todo esto crea la ilusión de la “inercia”. Sin embargo, nunca se debe basar el código en suponer que una hebra recién creada “tarda” en arrancar. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 60 V1.3 30 Arquitectura e Ingeniería de Computadores Errores frecuentes Competición entre las hebras. La competición se da cuando dos o más hebras quieren llegar o hacer lo mismo a la vez. Sólo una de ellas gana la competición. Cuál gana viene dado por muchos factores, muchos de ellos fuera de control. No olvidar que: una hebra puede ser interrumpida en cualquier punto arbitrario por un plazo indefinido de tiempo; no existe más orden entre ellas que el causado por el programador; planificación no es lo mismo que sincronización. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 61 V1.3 Arquitectura e Ingeniería de Computadores Errores frecuentes Evitar abrazos mortales Los abrazos mortales surgen por sincronización errónea. La hebra A tiene el recurso 1 y no puede seguir hasta tener el recurso 2; la hebra B tiene el recurso 2 y no puede seguir hasta tener el recurso 1. Los recursos más conflictivos suelen ser los mutexes. Se debe evitar bloquear más de un mutex simultáneamente. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 62 V1.3 31 Arquitectura e Ingeniería de Computadores Errores frecuentes Inversión de prioridad Este problema suele estar asociado a las aplicaciones de tiempo real. Ocurre cuando una hebra de alta prioridad es bloqueada en su ejecución por otra hebra de baja prioridad. Ejemplo: una hebra de baja prioridad bloquea un mutex; es interrumpida por una hebra de alta prioridad que quiere bloquear el mismo mutex y, por lo tanto, queda bloqueada; una tercera hebra de prioridad media impide la ejecución –y desbloqueo del mutex– de la hebra de baja prioridad; Resultado: la hebra de alta prioridad queda indefinidamente bloqueada. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 63 V1.3 Arquitectura e Ingeniería de Computadores Errores frecuentes Compartición de variables de estado en diferentes predicados. Cada variable debe usarse para comprobar una y sólo una condición. Si no se hace así, puede que la señalización de la condición despierte a la hebra que no esperaba por ella... y el programa se para. Compartición de variables privadas. Pila Se puede hacer siempre que se garantice que la función a la que pertenece la pila compartida no hace “return” mientras el resto de hebras usuarias no hayan terminado de hacer uso de ella. ¡Atención a los punteros no inicializados! © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 64 V1.3 32 Arquitectura e Ingeniería de Computadores Errores frecuentes Problemas de rendimiento. Atención al paralelismo “serializado”. Usar un sólo “gran” mutex para acceder a una librería es un paralelismo muy grosero... ... pero usar demasiados mutexes penaliza fuertemente el rendimiento. Para evitar contención con el cache, es bueno alinear y separar datos usados por diferentes hebras y que afecten sustancialmente al rendimiento. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 65 V1.3 Arquitectura e Ingeniería de Computadores Programa ejemplo Presentamos una aplicación para obtener la lista de números primos hasta una cierta cantidad. Utilizamos el paradigma de grupo de trabajo. Hacemos uso de mutexes, variables de estado, predicados, estados de cancelación, etc. © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 66 V1.3 33 Arquitectura e Ingeniería de Computadores Programa ejemplo /* Ejemplo de búsqueda de números primos */ #include #include #include #include <pthread.h> <stdio.h> <stdlib.h> <errno.h> /* Constantes usadas */ #define workers 5 /* Hebras que realizan la búsqueda */ #define request 110 /* Primos a encontrar */ /* * Macros */ #define check(status,string) if (status != 0) { \ errno = status; \ fprintf(stderr, "%s status %d: %s\n", \ string, status, strerror(status)); \ } © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 67 V1.3 Arquitectura e Ingeniería de Computadores Programa ejemplo /* Datos globales */ pthread_mutex_t prime_list = 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 */ int current_num = 1; int thread_hold = 1; int count = 1; int primes[request]; pthread_t threads[workers]; /* /* /* /* /* Siguiente número a comprobar */ Número asociado al estado */ Cuenta de números primos e índice a los mismos */ Reserva de primos – acceso sincronizado */ Matriz con las hebras trabajadoras */ static void unlock_cond(void *arg) { int status; status = pthread_mutex_unlock(&cond_mutex); check(status, "Mutex_unlock"); } © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 68 V1.3 34 Arquitectura e Ingeniería de Computadores Programa ejemplo /* 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 numerator; /* Usada para determinar la primalidad */ int denominator; /* Usada para determinar la primalidad */ int cut_off; /* Número a comprobar dividido por 2 */ int notifiee; /* Usada durante la cancelación */ int prime; /* Flag para indicar la primalidad */ int my_number; /* Identificador de la hebra trabajadora */ int status; /* Status de las llamadas a pthread_* */ int not_done = 1; /* Predicado del lazo de trabajo */ int oldstate; /* Estado de cancelado previo */ my_number = (int)arg; © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 69 V1.3 Arquitectura e Ingeniería de Computadores Programa ejemplo /* 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); © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 70 V1.3 35 Arquitectura e Ingeniería de Computadores Programa ejemplo /* Realiza las comprobaciones sobre números cada vez mayores hasta encontrar el número deseado de primos. */ while (not_done) { pthread_testcancel(); /* Comprobar petición de cancelación */ /* Obtener siguiente número a comprobar */ status = pthread_mutex_lock(&current_mutex); check(status, "Mutex_lock"); current_num = current_num + 2; /* Nos saltamos los pares */ numerator = current_num; status = pthread_mutex_unlock(&current_mutex); check(status, "Mutex_unlock"); /* Verificamos primalidad hasta número/2 */ cut_off = numerator/2 + 1; prime = 1; /* Comprobamos la divisibilidad */ for (denominator = 2;((denominator < cut_off) && (prime)); denominator++) prime = numerator % denominator; © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 71 V1.3 Arquitectura e Ingeniería de Computadores Programa ejemplo if (prime != 0) { /* 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(&prime_list); check(status, "Mutex_lock"); if (count < request) { primes[count] = numerator; count++; } else if (count == request) { not_done = 0; count++; for (notifiee = 0; notifiee < workers; notifiee++) { if (notifiee != my_number) { status = pthread_cancel(threads[notifiee]); check(status, "Cancel"); } } } © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 72 V1.3 36 Arquitectura e Ingeniería de Computadores Programa ejemplo status = pthread_mutex_unlock(&prime_list); check (status, "Mutex_unlock"); /* Permitir de nuevo cancelaciones */ pthread_setcancelstate(oldstate, &oldstate); } pthread_testcancel(); } return arg; } © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 73 V1.3 Arquitectura e Ingeniería de Computadores Programa ejemplo main() { int worker_num; /* Índice de trabajadores */ void *exit_value; /* Estado final para cada trabajador */ int list; /* Para imprimir la lista de primos encontrados */ int status; /* Status de las llamadas a pthread_* */ int index1; /* Para ordenar los primos */ int index2; /* Para ordenar los primos */ int temp; /* Parte de la ordenación */ int line_idx; /* Alineado de columna en salida */ /* 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"); } © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 74 V1.3 37 Arquitectura e Ingeniería de Computadores Programa ejemplo /* Poner a cero el predicado thread_hold y señalizar globalmente que los trabajadores pueden comenzar. */ 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"); © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 75 V1.3 Arquitectura e Ingeniería de Computadores Programa ejemplo /* 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); } © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 76 V1.3 38 Arquitectura e Ingeniería de Computadores Programa ejemplo /* 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. por las hebras trabajadoras y los las hebras han trabajado en paralelo no en que están almacenados los primos. Algoritmo de la burbuja: ¡lo siento! */ for (index1 = 1; index1 < request; index1++) { for (index2 = 0; index2 < index1; index2++) { if (primes[index1] < primes[index2]) { temp = primes[index2]; primes[index2] = primes[index1]; primes[index1] = temp; } } } © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 77 V1.3 Arquitectura e Ingeniería de Computadores Programa ejemplo /* Imprimir la lista de primos obtenidos. */ printf(“La lista de %d primos es la siguiente:\n2", request); for (list = 1, line_idx = 1; list < request; list++, line_idx++) { if (line_idx >= 4) { printf (",\n"); line_idx = 0; } else if (line_idx > 0) { printf(",\t"); } printf("%d", primes[list]); } printf("\n"); } © J. A. de Frutos Redondo, R. Durán 2006 6. Programación paralela de sistemas SMP 78 V1.3 39