Ejemplos java y C/linux

Tutoriales

Enlaces

Licencia

Creative Commons License
Esta obra está bajo una licencia de Creative Commons.
Para reconocer la autoría debes poner el enlace http://www.chuidiang.org

Algunos detalles sobre los Threads

Cómo hacer que un hilo espere a que termine otro

 Vamos a hacer un ejemplo de código en el que un hilo espera a que termine otro y recoge el valor que este le devuelve. Cualquier hilo puede esperar por cualquier otro hilo, no necesariamente tiene que ser el hilo padre el que espere por el hijo. En el ejemplo el hilo padre espera por el hijo, así que en el texto se seguirá esa convención.

A veces es necesario hacer que un hilo espere a otro/s hilo/s. Por ejemplo, supongamos que varios hilos están realizando un cálculo y es necesario el resultado el resultado de todos ellos para obtener el resultado total. El hilo encargado de este resultado total debe esperar a que todos los demás hilos terminen. Sin embargo, no todos los hilos son susceptibles de ser esperados.

    Hay hilos a los que el sistema libera automáticamente todos sus recursos cuando terminan. Con otros, sin embargo, no lo hace. El que el sistema haga una cosa u otra, depende de los atributos que le hayamos pasado a la hora de crear el hilo con pthread_create(). Por los primeros hilos, los que se liberan automáticamente, no podemos esperar. Por los segundos, los que mantienen sus recursos, sí podemos esperar. De hecho, si algún hilo no espera a que termine, sus recursos nunca se liberarán (salvo que se liberen explicitamente con la función pthread_detach()).

    Si ponemos los atributos de creación a NULL, el hilo es por defecto "esperable". De todas formas, vamos a poner explícitamente el atributo para ver cómo funciona.

Los atributos de un hilo son de tipo pthread_attr_t. Para rellenar esta estructura con sus valores por defecto, tenemos la función pthread_attr_init(). El código para obtener los atributos con su valor por defecto es

pthread_attr_t atributos;
...
pthread_attr_init (&atributos);

Ahora podemos cambiar cualquiera de los campos de esta estructura con funciones específicas para ello. En concreto, nos intersa la función pthread_attr_setdetachstate(), que es la que permite cambiar esta característica. Hay dos valores posibles para el atributo, PTHREAD_CREATE_JOINABLE y PTHREAD_CREATE_DETACHED. Con el primer valor podemos esperar por el hilo, con el segundo se libera automáticamente. El código quedaría

pthread_attr_setdetachstate (&atributos, PTHREAD_CREATE_JOINABLE);

Con esto ya podemos crear un thread al que se puede esperar con la función pthread_create(). Echa un ojo al ejemplo de la página de procesos e hilos para ver cómo se crea un hilo con esta función. Sólo hay que sustituir el NULL por &atributos.

Nuestro thread termina cuando sale de la función que está ejecutando o llamando a pthread_exit(). Si se hace de la primera manera, el sistema se encarga de llamar automáticamente a pthread_exit(). Si lo llamamos nosotros explícitamente, tenemos posibilidad de devolver un resultado al que espere que terminemos. El código para salir del thread es

pthread_exit ((void *)"Ya tá");

El parámetro que se pasa es el valor que se devolverá al hilo que espera. Como es un void *, puede ser un puntero a cualquier cosa que queramos. En el ejemplo lo hemos puesto a una cadena de caracteres fija. ¡OJO!, aquí no se pueden devolver punteros a variables locales, ya que al terminar el hilo y salir de la función, la variable local deja de existir. El "pobre" que recoja el puntero, lo tendrá apuntando a una zona de memoria que puede contener valores no válidos. Así que o devolvemos punteros a variables que no desaparezcan (globales, estáticas, etc) o a zonas de memoria que el propio hilo haya creado con malloc() (luego el hilo que espera deberá liberarlas con free()).

El hilo que espera, debe llamar a la función pthread_join(). Esta llamada hace que el hilo se "duerma" hasta que el otro hilo termine. Si el otro hilo ya había terminado, la función pthread_join() sale inmediatamente.

La función pthread_join() admite dos parámetros. El primero es el identificador pthread_t del hilo por el que queremos esperar. El segundo es un void **. Si le ponemos algo que no es NULL, en él nos devolverá lo que ha devuelto el hilo hijo. En nuestro ejemplo, como sabemos que nos van a devolver un char *, lo declararemos y eso será lo que pasemos.

pthread_t idHilo;
char *valorDevuelto = NULL;
...
pthread_join (idHilo, (void **)&valorDevuelto);

Esto es lo básico. En espera.c tienes el código de ejemplo. Puedes bajarte ese fichero, el Makefile, quitarles la extensión .txt y compilar con make espera. Luego ejecutas espera y verás como el hilo hijo espera 1 segundo y termina devolviendo una cadena. El padre, mientras tanto, se ha quedado bloqueado esperando la terminación del hijo. Luego escribe en pantalla el valor devuelto.

Acceso sincronizado a recursos comunes

Vamos a hacer un ejemplo de cómo dos hilos esperan el uno por el otro para acceder a los datos de un array de enteros. Uno de los hilos (el del main) pone todos los items del array con el valor 0, luego los rellena todos con el valor 1, después 2 y así sucesivamente. El otro hilo verifica que todos los valores sean iguales al de la posición 0, es decir, todos iguales, dando un error si no lo son.

Si no hay ningún tipo de sincronización, es fácil (cuanto mayor sea el array más fácil) que el hilo que escribe no haya terminado y empiece a leer el otro hilo, dando errores ya que no todos los items contendrán el mismo valor.

En general, al estar los dos hilos ejecutándose de forma independiente, es posible que uno intente acceder (leer o escribir) a una estructura de datos (o un fichero o cualquier otro recurso) cuando el otro hilo no ha acabado (de leer o escribir) en dicha estructura. Si, por ejemplo, uno de ellos empieza a escribir datos en una estructura y cuando está a medias el otro hilo se pone a leer, este segundo leera datos incoherentes.

En mutex.c, y compilando con Makefile y el comando make sinmutex se genera un ejecutable sinmutex. Ejecutándolo verás un ejemplo el ejemplo de cómo ocurre esto con un array de 1000 enteros. Acuerdate de quitar los .txt

Para sincronizar todo esto están los mutex (EXclusión MUTua, pero en inglés, del revés). Un mutex es como un semáforo que deja o no pasar al hilo. Antes de acceder a la estructura de datos, el hilo debe mirar el semáforo. Si el semáfor está rojo, el hilo se queda bloqueado hasta que se ponga verde. Si está verde, el hilo podrá acceder a la estructura de datos y el semáforo se pone rojo en cuanto pase este hilo. Una vez que termine de acceder a la estructura de datos, el hilo debe acordarse de poner verde el semáforo.

Para crear uno de estos semáforos tenemos la función pthread_mutex_init(). A esta función se le pasa un pthread_mutex_t * que luego servirá de identificador del semáforo (podemos crear tantos como necesitemos). Un segundo parámetro de la función son los atributos de creación del semáforo. Vale poner NULL para un comportamiento por defecto. El código para crear un mutex es:

pthread_mutex_t mutexBuffer;
...
pthread_mutex_init (&mutexBuffer, NULL);

Con el comportamiento por defecto, un mutex puede ponerlo en verde cualquier hilo, no necesariamente el que lo ha puesto rojo. Se puede cambiar este comportamiento con el segundo parámetro de pthread_mutex_init(). Con el comportamiento por defecto, sólo tiene efecto el poner rojo o verde el semáforo. Se puede cambiar este comportamiento para que se pueda poner rojo varias veces seguidas, de forma que cada vez se pone "más rojo". Para que finalmente se ponga verde, hace falta ponerlo verde tantas veces como se puso rojo anteriormente. Esto es un comportamiento recurisvo y sirve para hilos que utilicen recursivdad.

pthread_mutex_init() siempre devuelve 0.

Una vez inicializado el mutex, cuando un hilo quiera acceder a una estructura de datos que esté protegida por este mutex, debe llamar a la función pthread_mutex_lock(), pasándole el identificador del mutex. Si el semáforo está verde, se pone rojo y la función retorna inmediatamente, con lo que el hilo, en las siguientes líneas de código puede acceder a la estructura de datos.

Cuando termine con la estructura de datos, debe volver a poner verde el semáforo, llamando a la función pthread_mutex_unlock(), pasando de nuevo el identificador del mutex. El código quedaría

pthread_mutex_lock (mutexBuffer);
/* aquí se accede a la estructura de datos */
pthread_mutex_unlock (mutexBuffer);

 La función pthread_mutex_lock() puede bloquear al hilo hasta que alguien ponga el semáforo verde y esto, puede ser para toda la vida (depende de los otros hilos). Si es muy importante que un hilo no se quede bloqueado esperando un mutex, se puede llamar a la función pthread_mutex_trylock(), que se comorta igual que pthread_mutex_lock(), pero no deja bloqueado al hilo en caso de estar el semáforo rojo, sino que devuelve un error. El hilo que llama a esta función, debe verificar si se ha producido ese error o no antes de acceder a los datos.

Con esto queda todo listo. Cuando no se necesite más el mutex, se libera con pthread_mutex_destroy().

Con el mismo fuente de mutex.c y el mismo Makefile, pero compilando con make conmutex, se genera el ejecutable conmutex, que soluciona el problema de sinmutex. Los #ifdef que hay metidos por el código son para que se compilen o no las llamadas a las pthread_mutex_*().

En la página inicial tienes el libro de visitas, por si quieres hacer comentarios o sugerencias.

Estadísticas y comentarios

Numero de visitas desde el 4 Feb 2007: