Documento sobre hilos y mutex (Univ. Don Bosco)

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