G Guuííaa N Noo.. 66 Hilos y Mutex Asignatura : Ciclo : 02 – 2006 Lugar de ejecución: : Centro de Computo Sistemas Operativos Departamento de Informática Universidad Don Bosco Facultad de Ingeniería Escuela de Computación Objetivos. • • • • • Entender como se manejan los hilos en Linux. Crear hilos o thread en el sistema operativos Linux. Manejo de funciones y sus argumentos y variables con los hilos. Sincronización de hilos con mutex Manejo de funciones y sus argumentos y variables con mutex. Introducción. Parte I: Hilos Uno de los problemas más importantes en los ordenadores con un procesador es el de la ejecución simultánea de varios programas (procesos). Este paralelismo no se puede realizar de forma completa dada la exclusividad del procesador y de las zonas de memoria que necesita cada programa. Un método para lograr el paralelismo consiste en hacer que varios procesos cooperen y se sincronicen mediante memoria compartida. Otra alternativa es el empleo de múltiples hilos de ejecución en un solo espacio de direcciones. Este es el objetivo principal de la utilización de hilos. Un hilo (thread, en ingles) es un semi-proceso, que tiene su propia pila y que ejecuta una pieza de código dada. A diferencia de un proceso real, el hilo comparte normalmente la memoria con otros hilos (por lo que respecta a los procesos, estos se ejecutan en diferentes áreas de memoria). Un grupo de hilos es un conjunto de hilos ejecutándose dentro del mismo proceso. Todos ellos comparten la misma memoria, y así pueden acceder a las mismas variables globales, la misma área de la memoria, el mismo conjunto de descriptores de archivos, etc. Todos estos hilos se ejecutan en paralelo. La ventaja de usar un grupo de hilos en vez de usar un programa secuencial normal es que muchas de las operaciones pueden ser llevadas a cabo en paralelo, y así estos eventos pueden ser manejados inmediatamente cuando llegan. La ventaja de usar un grupo de hilos en vez de usar un grupo de procesos es que el intercambio de contexto entre los hilos es mucho mas rápido que el intercambio de contexto entre procesos (el intercambio de contexto significa que el sistema intercambia de la ejecución un hilo o Página 1/1 proceso, a otro hilo o proceso). También, la comunicación entre dos hilos es a menudo más rápida y más fácil de implementar que la comunicación entre dos procesos. Por otra parte, debido a que los todos los hilos en un grupo usan el mismo espacio de memoria, si uno de ellos corrompe el contenido de la memoria, los otros hilos podría sufrir lo mismo. Con los procesos, el sistema operativo normalmente protege los procesos de otros, y así, si uno corrompe su propio espacio de memoria, el resto de procesos no se perjudicarán. Otra ventaja del uso de procesos es que ellos corren en diferentes maquinas, mientras que los hilos tienen que correr en la misma maquina. Entre algunas de sus aplicaciones están: Utilización de los hilos en servidores. Los servidores pueden utilizar las ventajas del multihilo, creando un hilo gestor diferente para cada petición entrante de un cliente. Utilización de los hilos en interfaces de usuario. Se pueden obtener aumentos de rendimiento empleando un hilo para interactuar con un usuario, mientras se pasan las peticiones a otros hilos para su ejecución. Utilización de los hilos en el diseño de un kernel multihilo: para un sistema operativo distribuido que distribuya diferentes tareas entre los hilos. Creando y manipulando Hilos. int pthread_create(pthread_t * thread, pthread_attr_t *attr, void * (*start_routine)(void *), void *arg) Cuando un programa empieza a ejecutarse, tiene uno de sus hilos corriendo, el cual ejecuta la función main() del programa. Este ya es un hilo hecho y derecho, que tiene su propio id de hilo. Para crear un nuevo hilo, el programa debe usar la función pthread_create(). • thread: Es una variable del tipo pthread_t que contendrá los datos del thread y que nos servirá para identificar el hilo (thread) en concreto cuando nos interese hacer llamadas a la librería para llevar a cabo alguna acción sobre él. • attr: Es un parámetro del tipo pthread_attr_t y que se debe inicializar previamente con los atributos que queramos que tenga el hilo (thread). Entre los atributos hay la prioridad, el quantum, el algoritmo de planificación que queramos usar, etc. Si pasamos como parámetro aquí NULL, la librería le asignará al hilo (thread) unos atributos por defecto. • start_routine: Aquí pondremos la dirección de la función que queremos que ejecute el hilo (thread). La función debe devolver un puntero genérico (void *) como resultado, y debe tener como único parámetro otro puntero genérico. La ventaja de que estos dos punteros sean genéricos es que podremos devolver cualquier cosa que se nos ocurra mediante los castings de tipos necesarios. Si necesitamos pasar o devolver más de un parámetro a la vez, se puede crear una estructura y meter allí dentro todo lo que necesitemos. Luego pasaremos o devolveremos la dirección de esta estructura como único parámetro. • arg: Es un puntero al parámetro que se le pasará a la función. Puede ser NULL si no queremos pasarle nada a la función. En caso de que todo haya ido bien, la función devuelve un 0 ó un valor distinto de 0 en caso de algún error. Página 2/2 void pthread_exit(void *retval) Esta función termina la ejecución del thread que la llama. • • retval: Es un puntero genérico a los datos que queremos devolver como resultado. Estos datos serán recogidos más tarde cuando alguien haga un pthread_join con nuestro identificador de thread. No devuelve ningún valor. int pthread_join(pthread_t th, void **thread_return) Esta función suspende el thread llamante hasta que no termine su ejecución el thread indicado por th. Además, una vez éste último termina, pone en thread_return el resultado devuelto por el thread que se estaba ejecutando. • th: Es el identificador del thread que queremos esperar, y es el mismo que obtuvimos al crearlo con pthread_create. • thread_return: Es un puntero a puntero que apunta (valga la redundancia) al resultado devuelto por el thread que estamos esperando cuando terminó su ejecución. Si este parámetro es NULL, le estamos indicando a la librería que no nos importa el resultado. • Devuelve 0 en caso de todo correcto, o valor diferente de 0 si hubo algún error. Procedimiento. Programa: Hilo1.c #include <pthread.h> #include <stdlib.h> #include <unistd.h> void *funcion_hilo(void *arg) { int i; for ( i=0; i<20; i++ ) { printf("Hilo #%d\n",i); sleep(1); } return NULL; } int main(void) { pthread_t mihilo; if ( pthread_create( &mihilo, NULL, funcion_hilo, NULL) ) { printf("error creando el hilo."); abort(); } if ( pthread_join ( mihilo, NULL ) ) { printf("Error uniendo los hilos."); abort(); } exit(0); } Para compilar el programa, escriba: [usuario@linux ~/bin]$ gcc -o hilo1 hilo1.c –lpthread Para correr el archivo, escriba: [usuario@linux ~/bin]$ ./hilo1 Página 3/3 En la función main se ha declarado la variable mihilo, que es del tipo pthread_t (definido en la librería pthread.h). El tipo pthread_t es llamado “id del hilo” y es usado como una especie de identificador o manejador del hilo (thread). Después de la declaración de mihilo (que va a manejar el hilo (thread) que se va a crear), llamamos a la función pthread_create para darle vida a un hilo (thread) real. Se ha colocado la función pthread_create dentro de un if() debido a que esta función devuelve un cero si el hilo (thread) fue creado exitosamente y un valor diferente de cero si fallo la creación del hilo (thread). Con esto se puede detectar si el llamado de la función pthread_create falló o no. El primer argumento de la función es un puntero a la variable mihilo, &mihilo. El segundo es un para este caso en el valor de NULL, pero puede ser usado para definir ciertos atributos de los hilos (thread). Debido a que los atributos predeterminados sirven para este ejemplo, se dejara el valor de NULL. El tercer argumento, es el nombre de la función que el nuevo hilo (thread) ejecutará cuando este empiece. En este caso el nombre de la función es funcion_hilo. Cuando la función retorne algún valor, será cuando el hilo (thread) terminará. En es caso la función funcion_hilo solo imprime en pantalla “hilo # “ 20 veces y luego termina. Note que la función acepta un void * como argumento y también retorna un void * como valor de retorno. Esto indica que es posible usar un void * para pasar una pieza arbitraria de datos al nuevo hilo (thread) creado, que el nuevo hilo (thread) retornara una pieza arbitraria de datos cuando termine. Y el cuarto argumento se utiliza para pasar al thread los argumentos de la función. En este caso es el valor de NULL por que no necesitamos pasar ningún dato a la función. Vale la pena mencionar que este programa consiste de dos hilos (threads), ya que la función main también es considerada como un hilo (thread). también, vale la pena preguntarse: que pasa después de que el nuevo hilo es creado? Pues el programa prosigue a ejecutar la siguiente línea “if ( pthread_join() ) “. Y que pasa cuando el nuevo hilo (thread) termina? Simplemente se detiene y espera a ser unido a otro proceso como parte de su proceso de limpieza. Al ejecutarse la línea if ( pthread_join() ), así como pthread_create divide un solo hilo (thread) en dos hilos (threads), la función pthread_join une los hilos (threads) en uno solo. El primer argumento de la función pthread_join es el id del hilo – o sea la variable mihilo – y el segundo argumento es un puntero a un puntero void. Si el puntero void no es NULL, entonces la función colocara el valor retornado en la localización especificada (una variable por ejemplo). Notara que funcion_hilo le toma 20 segundos en terminar. En todo ese tiempo la función main ya ha llamado a la función pthread_join. Cuando esto ocurra la función main será bloqueada (se detendrá) y esperara hasta que funcion_hilo termine de ejecutarse. Cuando funcion_hilo termina entonces pthread_join retornara y el programa ejecutara main otra vez. Si los hilos creados no son unidos con la función pthread_join, eventualmente la creación de hilos fallara. Página 4/4 Parte II: Mutex Un mútex consiste en una especie de semáforo binario con dos estados, cerrado y no cerrado. Un mútex es un objeto que permite a los hilos asegurar la integridad de un recurso compartido al que tienen acceso. Tiene dos estados : bloqueado y desbloqueado. Sobre un mútex se pueden realizar las siguientes operaciones: Lock: Intenta cerrar el mútex. Si el mútex no está cerrado, se cierra, todo ello en una acción atómica. Si el mútex está cerrado, el hilo se bloquea. Si dos hilos intentan cerrar el mútex al mismo tiempo, cosa que sólo puede pasar en un multiprocesador real, uno de ellos lo consigue y el otro no, bloqueándose. La forma de regular esto depende de la implementación. Unlock: Elimina o libera el cierre del mútex. Si existe uno o más hilos esperando por el mútex, se desbloquea exactamente uno, y el resto permanece bloqueado a la espera. Antes de acceder a un recurso compartido un hilo debe bloquear un mútex. Si el mútex no ha sido bloqueado antes por otro hilo, el bloqueo es realizado. Si el mútex ha sido bloqueado antes, el hilo es puesto a la espera. Tan pronto como el mútex es liberado, uno de los hilos en espera a causa de un bloqueo en el mútex es seleccionado para que continúe su ejecución, adquiriendo el bloqueo. Un ejemplo de utilización de un mútex es aquél en el que un hilo A y otro hilo B están compartiendo un recurso típico, como puede ser una variable global. El hilo A bloquea el mútex, con lo que obtiene el acceso a la variable. Cuando el hilo B intenta bloquear el mútex, el hilo B es puesto a la espera puesto que el mútex ya ha sido bloqueado antes. Cuando el hilo A finaliza el acceso a la variable global, desbloquea el mútex. Cuando esto suceda, el hilo B continuará la ejecución adquiriendo el bloqueo, pudiendo entonces acceder a la variable. Hilo A : lock(mutex) acceso al recurso unlock(mutex) Hilo B : lock(mutex) acceso al recurso unlock(mutex) Un hilo puede adquirir un mútex no bloqueado. De esta forma, la exclusión mutua entre hilos del mismo proceso está garantizada, hasta que el mútex es desbloqueado permitiendo que otros hilos protejan secciones críticas con el mismo mútex. Si un hilo intenta bloquear un mútex que ya está bloqueado, el hilo se suspende. Si un hilo desbloquea un mútex y otros hilos están esperando por el mútex, el hilo en espera con mayor prioridad obtendrá el mútex. La tabla 1 muestra un resumen de las funciones mas importantes para la creación y manipulación de hilos y mutex: Tabla 1. Cuadro resumen de funciones para hilos y mutex Página 5/5 Servicios POSIX Mutex #include<pthread.h> int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexaddr_t *attr); mutex: variable de tipo mutex. attr: Especifica los atributos con los que se crea el mutex inicialmente, en caso de que este argumento sea NULL, se tomaran los atributos por defecto. Descripción: Permite iniciar una variable de tipo mutex. #include<pthread.h> int pthread_mutex_destroy(pthread_mutex_t *mutex); Descripción: Permite destruir un objeto de tipo mutex. #include<pthread.h> int pthread_mutex_lock(pthread_mutex_t *mutex); Descripción: Corresponde con la operación lock. Intenta obtener el mutex. Si el mutex ya se encuentra adquirido por otro proceso, el proceso ligero que ejecuta la llamada se bloquea. #include<pthread.h> int pthread_mutex_unlock(pthread_mutex_t *mutex); Descripción: Corresponde con la operación unlock y permite al proceso ligero que la ejecuta liberar el mutex. #include<pthread.h> int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr); Descripción: Esta función inicia una variable de tipo condicional. attr: Especifica los atributos con los que se crea inicialmente la variable condicional, en caso de que este argumento sea NULL, se tomaran los atributos por defecto. #include<pthread.h> int pthread_cond_destroy(pthread_cond_t *cond, pthread_condattr_t *attr); Descripción: Permite destruir una variable de tipo condicional. Página 6/6 #include<pthread.h> int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); Descripción: Suspende al proceso ligero hasta que otro proceso ejecute una operación c_signal sobre la variable condicional pasada como primer argumento. De forma atómica se libera el mutex pasado como segundo argumento. Cuando el proceso se despierta volverá a competir por el mutex. #include<pthread.h> int pthread_cond_signal(pthread_cond_t *cond); Descripción: Desbloquea a un proceso suspendido en la variable condicional pasada como argumento a esta funcion. Esta funcion no tiene efecto #include<pthread.h> int pthread_cond_broadcast(pthread_cond_t *cond); Descripción: Desbloquea a todos los procesos ligeros suspendidos en una variable condicional. Procedimiento. El siguiente programa es una muestra del problema que puede surgir con la falta de sincronización entre procesos ligeros. El programa lo que pretende hacer es que tres hilos impriman cada uno una serie de 20 caracteres (cada hilo un carácter distinto) en pantalla, incluyendo la función main la cual se considera como un hilo. Programa: hilo2.c #include #include #include #include <pthread.h> <stdlib.h> <unistd.h> <stdio.h> void *thread_function01(void *arg) { int i; for ( i=0; i<20; i++ ) { printf("."); fflush(stdout); sleep(1); Página 7/7 } } return NULL; void *thread_function02(void *arg) { int i; for ( i=0; i<20; i++ ) { printf("o"); fflush(stdout); sleep(2); } return NULL; } void *thread_function03(void *arg) { int i; for ( i=0; i<20; i++ ) { printf("+"); fflush(stdout); sleep(1); } return NULL; } int main(void) { pthread_t mithread01, mithread02, mithread03; int i; if ( pthread_create( &mithread01, NULL, thread_function01, NULL) ) { printf("Error creando el hilo."); abort(); } if ( pthread_create( &mithread02, NULL, thread_function02, NULL) ) { printf("Error creando el hilo."); abort(); } if ( pthread_create( &mithread03, NULL, thread_function03, NULL) ) { printf("Error creando el hilo."); abort(); } for ( i=0; i<20; i++) { } printf("x"); fflush(stdout); sleep(3); exit(0); } Página 8/8 Para compilar el programa, escriba: [usuario@linux ~/bin]$ gcc -o hilo2 hilo2.c –lpthread Para correr el archivo, escriba: [usuario@linux ~/bin]$ ./hilo2 La asignación para esta guía es modificar el programa hilo2.c para que cada proceso ligero incluyendo la función main, puedan imprimir en pantalla la serie de 20 caracteres correspondiente a cada uno de ellos sincronizadamente haciendo uso de Mutex. Bibliografía. Sistemas Operativos Jesús Carretero Pérez McGrawHill Página 9/9