Renuncia de responsabilidad:
La versión original de este artículo fue publicada por IBM
developerWorks y es propiedad de Westtech Information Services. Este
documento es una versión actualizada del artículo original y contiene
mejoras introducidas por el Equipo de Documentación de Gentoo.
Este documento carece de soporte activo.
|
Explicación de los hilos POSIX, parte 3
1.
Mejorar la eficiencia con variables condicionales
Explicación de las variables condicionales
Finalicé mi anterior artículo
describiendo un dilema concreto acerca de cómo trata un hilo una situación en
la que está esperando a que se cumpla una condición. Puede bloquear y
desbloquear el mutex repetidamente, comprobando una cierta estructura de datos
para verificar la presencia de un valor. Pero eso sería una pérdida de tiempo y
de recursos, esta forma de manejar los datos en un bucle muy ocupado es
extremadamente ineficaz. La mejor forma de hacerlo es usar la llamada
pthread_cond_wait() para esperar a que una determinada condición sea cierta.
Es muy importante comprender lo que hace pthread_cond_wait() -- es el corazón
del sistema de señalización de los hilos POSIX y, también, la parte más difícil
de comprender.
Primero, consideremos un escenario donde un hilo ha bloqueado un mutex, para
ver una lista enlazada, y resulta que la lista está vacía. Este hilo en
concreto no podrá hacer nada -- está diseñado para eliminar un nodo de la
lista, pero no hay nodos disponibles. Así que esto es lo que hace.
Mientras que mantiene el mutex bloqueado, nuestro hilo llamará a
pthread_cond_wait(&mycond,&mymutex). La llamada pthread_cond_wait()
es bastante compleja, así que veremos cada una de sus operaciones, una por una.
La primera cosa que pthread_cond_wait() hace es desbloquear simultáneamente el
mutex mymutex (para que otros hilos puedan modificar la lista enlazada) y
esperar a la condición mycond (para que pthread_cond_wait() despierte cuando
reciba una señal desde otro hilo). Ahora que el mutex está desbloqueado, otros
hilos pueden acceder y modificar la lista enlazada, posiblemente añadiendo
elementos a la misma.
En este momento, la llamada pthread_cond_wait() aún no ha retornado. El
desbloqueo del mutex ocurre inmediatamente, pero esperar a la condición mycond
es normalmente una operación de bloqueo, lo cual significa que nuestro hilo se
irá a dormir, sin consumir ciclos de CPU hasta que se despierte. Esto es
exactamente lo que queremos que ocurra. Nuestro hilo está durmiendo esperando a
que se cumpla una determinada condición, sin realizar ningún bucle muy ocupado
que desperdicie tiempo de la CPU. Desde la perspectiva de nuestro hilo,
sencillamente está esperando a que retorne la llamada pthread_cond_wait().
Ahora, para continuar con la explicación, digamos que otro hilo (lo llamaremos
"thread 2") bloquea mymutex y añade un elemento a nuestra lista enlazada.
Inmediatamente después de desbloquear el mutex, "thread 2" llama a la función
pthread_cond_broadcast(&mycond). Haciendo esto, daremos lugar a que todos
los hilos esperando a la variable condicional mycond despierten. Esto significa
que nuestro primer hilo (que se encuentra en plena llamada pthread_cond_wait())
despertará.
Ahora, veamos lo que ocurre con nuestro primer hilo. Después de que "thread 2"
ha llamado a pthread_cond_broadcast(&mymutex) puede pensarse que
pthread_cond_wait() del primer hilo retornará inmediatamente. ¡Ni mucho menos!,
en su lugar pthread_cond_wait() realizará una última operación: volver a
bloquear el mutex. Una vez que pthread_cond_wait() tiene el bloqueo de nuevo,
entonces retornará y permitirá al primer hilo seguir ejecutándose. En este
momento, puede comprobar inmediatamente la lista para comprobar cualquier
cambio interesante.
Paremos y demos un repaso
Listado de Código 1.1: queue.h |
/* queue.h
*/
typedef struct node {
struct node *next;
} node;
typedef struct queue {
node *head, *tail;
} queue;
void queue_init(queue *myroot);
void queue_put(queue *myroot, node *mynode);
node *queue_get(queue *myroot);
|
Listado de Código 1.2: queue.c |
/* queue.c
*/
#include <stdio.h>
#include "queue.h"
void queue_init(queue *myroot) {
myroot->head=NULL;
myroot->tail=NULL;
}
void queue_put(queue *myroot,node *mynode) {
mynode->next=NULL;
if (myroot->tail!=NULL)
myroot->tail->next=mynode;
myroot->tail=mynode;
if (myroot->head==NULL)
myroot->head=mynode;
}
node *queue_get(queue *myroot) {
//get from root
node *mynode;
mynode=myroot->head;
if (myroot->head!=NULL)
myroot->head=myroot->head->next;
return mynode;
}
|
Listado de Código 1.3: control.h |
#include <pthread.h>
typedef struct data_control {
pthread_mutex_t mutex;
pthread_cond_t cond;
int active;
} data_control;
|
Listado de Código 1.4: control.c |
/* control.c
*/
#include "control.h"
int control_init(data_control *mycontrol) {
int mystatus;
if (pthread_mutex_init(&(mycontrol->mutex),NULL))
return 1;
if (pthread_cond_init(&(mycontrol->cond),NULL))
return 1;
mycontrol->active=0;
return 0;
}
int control_destroy(data_control *mycontrol) {
int mystatus;
if (pthread_cond_destroy(&(mycontrol->cond)))
return 1;
if (pthread_mutex_destroy(&(mycontrol->cond)))
return 1;
mycontrol->active=0;
return 0;
}
int control_activate(data_control *mycontrol) {
int mystatus;
if (pthread_mutex_lock(&(mycontrol->mutex)))
return 0;
mycontrol->active=1;
pthread_mutex_unlock(&(mycontrol->mutex));
pthread_cond_broadcast(&(mycontrol->cond));
return 1;
}
int control_deactivate(data_control *mycontrol) {
int mystatus;
if (pthread_mutex_lock(&(mycontrol->mutex)))
return 0;
mycontrol->active=0;
pthread_mutex_unlock(&(mycontrol->mutex));
pthread_cond_broadcast(&(mycontrol->cond));
return 1;
}
|
Tiempo para la depuración
Un archivo más, antes de que sigamos con el más importante. Aquí tenemos
dbug.h:
Listado de Código 1.5: dbug.h |
#define dabort() \
{ printf("Aborting at line %d in source file %s\n",__LINE__,__FILE__); abort(); }
|
Usaremos este código para resolver errores irrecuperables en nuestro código.
El código del conjunto de trabajo
Hablando del conjunto de trabajo, aquí está:
Listado de Código 1.6: workcrew.c> |
#include <stdio.h>
#include <stdlib.h>
#include "control.h"
#include "queue.h"
#include "dbug.h"
/* */
struct work_queue {
data_control control;
queue work;
} wq;
/* */
typedef struct work_node {
struct node *next;
int jobnum;
} wnode;
/* */
struct cleanup_queue {
data_control control;
queue cleanup;
} cq;
/* */
typedef struct cleanup_node {
struct node *next;
int threadnum;
pthread_t tid;
} cnode;
void *threadfunc(void *myarg) {
wnode *mywork;
cnode *mynode;
mynode=(cnode *) myarg;
pthread_mutex_lock(&wq.control.mutex);
while (wq.control.active) {
while (wq.work.head==NULL && wq.control.active) {
pthread_cond_wait(&wq.control.cond, &wq.control.mutex);
}
if (!wq.control.active)
break;
//we got something!
mywork=(wnode *) queue_get(&wq.work);
pthread_mutex_unlock(&wq.control.mutex);
//perform processing...
printf("Thread number %d processing job %d\n",mynode->threadnum,mywork->jobnum);
free(mywork);
pthread_mutex_lock(&wq.control.mutex);
}
pthread_mutex_unlock(&wq.control.mutex);
pthread_mutex_lock(&cq.control.mutex);
queue_put(&cq.cleanup,(node *) mynode);
pthread_mutex_unlock(&cq.control.mutex);
pthread_cond_signal(&cq.control.cond);
printf("thread %d shutting down...\n",mynode->threadnum);
return NULL;
}
#define NUM_WORKERS 4
int numthreads;
void join_threads(void) {
cnode *curnode;
printf("joining threads...\n");
while (numthreads) {
pthread_mutex_lock(&cq.control.mutex);
/* */
while (cq.cleanup.head==NULL) {
pthread_cond_wait(&cq.control.cond,&cq.control.mutex);
}
/* */
curnode = (cnode *) queue_get(&cq.cleanup);
pthread_mutex_unlock(&cq.control.mutex);
pthread_join(curnode->tid,NULL);
printf("joined with thread %d\n",curnode->threadnum);
free(curnode);
numthreads--;
}
}
int create_threads(void) {
int x;
cnode *curnode;
for (x=0; x<NUM_WORKERS; x++) {
curnode=malloc(sizeof(cnode));
if (!curnode)
return 1;
curnode->threadnum=x;
if (pthread_create(&curnode->tid, NULL, threadfunc, (void *) curnode))
return 1;
printf("created thread %d\n",x);
numthreads++;
}
return 0;
}
void initialize_structs(void) {
numthreads=0;
if (control_init(&wq.control))
dabort();
queue_init(&wq.work);
if (control_init(&cq.control)) {
control_destroy(&wq.control);
dabort();
}
queue_init(&wq.work);
control_activate(&wq.control);
}
void cleanup_structs(void) {
control_destroy(&cq.control);
control_destroy(&wq.control);
}
int main(void) {
int x;
wnode *mywork;
initialize_structs();
/* CREATION */
if (create_threads()) {
printf("Error starting threads... cleaning up.\n");
join_threads();
dabort();
}
pthread_mutex_lock(&wq.control.mutex);
for (x=0; x<16000; x++) {
mywork=malloc(sizeof(wnode));
if (!mywork) {
printf("ouch! can't malloc!\n");
break;
}
mywork->jobnum=x;
queue_put(&wq.work,(node *) mywork);
}
pthread_mutex_unlock(&wq.control.mutex);
pthread_cond_broadcast(&wq.control.cond);
printf("sleeping...\n");
sleep(2);
printf("deactivating work queue...\n");
control_deactivate(&wq.control);
/* CLEANUP */
join_threads();
cleanup_structs();
}
|
Análisis del código
Ahora es el momento de analizar detenidamente el código. La primera struct
definida se llama "wq" y contiene data_control y una cabecera de cola. La
estructura data_control será usada para manejar el acceso a toda la cola,
incluyendo los nodos en la cola. Nuestro siguiente trabajo es definir los nodos
activos. Para reducir el código al propósito de este artículo, todo ello se
mantendrá como un número de trabajo.
Después creamos la cola de limpieza. Los comentarios explican cómo funciona.
Bien, evitemos por ahora las llamadas threadfunc(), join_threads(),
create_threads() e initialize_structs() y volvamos a main(). Lo primero que
hacemos es inicializar nuestras estructuras -- esto incluye nuestros
data_controls y las colas, al igual que activar nuestra cola de trabajo.
Limpieza especial
Es el momento de inicializar nuestros hilos. Si se observa nuestra llamada
create_threads(), todo parecerá absolutamente normal -- exceptuando una cosa.
Hay que observar que estamos localizando un nodo de limpieza, e inicializando
sus componentes treadnum y TID. También pasamos un nodo de limpieza a cada hilo
en funcionamiento como argumento inicial. ¿Por qué hacemos esto?
Porque cuando un hilo en funcionamiento acaba, añadirá su nodo de limpieza a la
cola y terminará. Entonces, nuestro hilo principal detectará esta adición a la
cola de limpieza (por el uso de una variable condicional) y eliminará de la
cola el nodo. Dado que el TID (id del hilo) se almacena en el nodo de limpieza,
nuestro hilo principal sabrá exactamente el hilo que ha terminado. Entonces, el
hilo principal llamará a pthread_join(tid), y lo unirá a la correspondiente
hebra de trabajo. De no haber realizado esta acción, nuestro hilo principal
debería unirse a otros hilos que están funcionando en un orden arbitrario,
posiblemente el orden en que fueron creados. Dado que los hilos no necesitan
concluir en este orden, nuestro hilo principal podría estar aguardando a unirse
con un hilo mientras que podría haberse unido a otros diez distintos. ¿Puede
verse cómo esta decisión de diseño puede acelerar la conclusión de nuestro
código, especialmente si se está trabajando con cientos de hilos funcionales?
Creación de trabajo
Ahora que hemos empezado con nuestros hilos funcionales (y están encargándose
de realizar su trabajo con threadfunc(), al que llegaremos en un momento),
nuestro hilo principal empieza a insertar elementos en la cola de trabajo.
Primero, bloquea el mutex de control "wq", después localiza 16000 paquetes de
trabajo, insertándolos en la cola uno por uno. Después de realizar esto, se
llama a pthread_cond_broadcast(), para que todos los hilos dormidos despierten
y realicen su trabajo. Al acabar, nuestro hilo principal se duerme durante dos
segundos y después desactiva la cola de trabajo, diciéndoles a los hilos en
funcionamiento que terminen. Es cuando nuestro hilo principal llama a la
función join_threads() para limpiar todos los hilos en funcionamiento.
threadfunc()
Tiempo para ver threadfunc(): el código que cada hilo en ejecución lleva a
cabo. Cuando un hilo comienza su ejecución, inmediatamente bloquea el mutex de
la cola de trabajos, obtiene un nodo de trabajo (si se encuentra disponible) y
lo procesa. De no encontrarse disponible se llama a pthread_cond_wait(). Puede
notarse que se le llama en un bucle while() muy ligero, lo cual es muy
importante. Cuando se despierta desde una llamada pthread_cond_wait(), nunca
debe asumirse que la condición es definitivamente cierta -- muy probablemente
será cierta, pero puede no serlo. El bucle while ocasionará que se llame de
nuevo a pthread_cond_wait() si el hilo se despertó equivocadamente y la lista
continúa vacía.
Si hay un nodo trabajando, sencillamente imprimimos su número de trabajo, lo
liberamos y salimos. Código real podría hacer algo más sustancial. Al final del
bucle while(), bloquearemos el mutex para que podamos comprobar la variable
activa al igual que los nodos activos al comienzo del bucle. Si se ha seguido
el código exactamente, puede verse que si wq.control.active es 0, el bucle
while terminará y comenzará el código al final de threadfunc().
La parte del hilo funcional con el código de limpieza es muy interesante.
Primero desbloquea la cola de trabajo, dado que pthread_cond_wait() nos retorna
el mutex bloqueado. Entonces, crea un bloqueo en la cola de limpieza, añade
nuestro nodo de limpieza (que contiene nuestro TID, que el hilo principal usará
para su llamada pthread_join()), y entonces desbloquea la cola de limpieza.
Después de esto, indica a todo aquello esperando a cq
(pthread_cond_signal(&cq.control.cond)) que el hilo principal sabe que hay
un nuevo nodo que procesar. No usamos pthread_cond_broadcast() dado que no es
necesario -- solo un hilo (el hilo principal) está esperando a nuevas entradas
en la cola de limpieza. Nuestro hilo en funcionamiento emite un mensaje de
finalización y después concluye, esperando a ser pthread_joined() por el hilo
principal, cuando llama a join_threads().
join_threads()
Si se quiere ver un sencillo ejemplo de cómo las variables de condición deben
usarse, debemos echar un vistazo a la función join_threads(). Mientras que
tenemos hilos en ejecución, los bucles join_threads(), esperando a nuevos nodos
de limpieza en nuestra cola de limpieza. Si hay un nuevo nodo, quitamos de la
cola el nuevo nodo, desbloqueamos la cola de limpieza (para que otros nodos de
limpieza puedan ser añadidos por nuestros hilos en ejecución), lo unimos a
nuestro nuevo hilo (usando el TID almacenado en el nodo de limpieza), liberamos
el nodo de limpieza, decrementamos el número de hilos "activos" y continuamos.
Conclusión
Hemos llegado al final de la serie "Explicación de los hilos POSIX", espero que
estemos listos para empezar a añadir código multi-hilos a nuestras propias
aplicaciones. Para más información, puede consultarse la sección Recursos, que también contiene un tarball de todo el
código fuente mostrado en este artículo.
2.
Recursos
-
Un tarball del código
fuente usado en este artículo está disponible.
-
Leer la explicación de los hilos POSIX de Daniel
Parte 1 y Parte 2.
-
Leer la agradable documentación del manual de pthread Linux (man -k
pthread) es un excelente recurso.
-
Para un tratamiento en profundidad de los hilos POSIX, recomiendo este
libro:
Programming with POSIX Threads, por David R. Butenhof (Addison-Wesley,
1997). Quizá sea este el mejor libro disponible acerca de los hilos POSIX.
-
Los hilos POSIX también se cubren en este libro:
UNIX Network Programming - Networking APIs: Sockets and XTI, por
W. Richard Stevens (Prentice Hall, 1997). Este es un libro clásico, pero
no cubre los hilos POSIX con tanto detalle como el anterior: Programming
with POSIX Threads.
-
Ver la documentación de los hilos
Linux, por Sean Walton, KB7rfa.
-
Hay un tutorial
de hilos POSIX, por Mark Hays de la Universidad de Arizona.
-
En Una Introducción a
Pthreads-Tcl, pueden verse los cambios realizados a Tcl para
habilitarle el uso de hilos POSIX
-
Otro tutorial,
Iniciación a los hilos POSIX, por Tom Wagner y Don Towsley del
Departamento de Ciencia Informática en la Universidad de Masssachusetts,
Amherst
-
FSU PThreads
es una librería C que implementa los hilos POSIX a SunOS 4.1.x, Solaris 2.x,
SCO UNIX, FreeBSD, Linux, y DOS
-
La página principal de hilos POSIX y DCE para
Linux
-
Ver La Librería
LinuxThreads
-
Proolix
es un simple sistema operativo compatible con POSIX para i8086+ en
permanente desarrollo
|
|
Página actualizada 9 de octubre, 2005 |
Sumario:
En este artículo, el último de una serie de tres, acerca de los hilos POSIX,
Daniel explica cómo usar las variables condicionales. Las variables
condicionales son estructuras de hilos POSIX que permiten "despertar" hilos
cuando se cumplen ciertas condiciones. Puede pensarse en ellas como una forma
segura de señalización en los hilos. Daniel traza este artículo usando todo lo
que hemos aprendido para implementar un conjunto de trabajo con multi-hilos.
|
Daniel Robbins
Autor
Fernando M. Bueno
Traductor
|
|
Donate to support our development efforts.
|
|
|