Sincronización de Hilos POSIX: Variables de Condición

Las variables de condición son otro mecanismo de sincronización entre hilos. Las variables de condición usadas en conjunto con mutex permiten a un hilo esperar por la ocurrencia de una condición arbitraria. La espera es libre de carreras críticas.

En principio podríamos pensar que teniendo posesión de un mutex, podríamos consultar por el cambio de la variable que esperamos. Una vez hecha la consulta deberíamos liberar el mutex. El problema es que estamos obligados a hacer un loop esperando por el cambio de la variable. Esto es una espera activa (o espera ocupada) la cual consume mucha CPU.

Visibles a ambas hebras:
  pthread_mutex_t   lock_de_mi_variable = PTHREAD_MUTEX_INITIALIZER;
  int variable = 0;

En una hebra:
   :
  int done = 0;
  while (!done) {  /*Espera activa u ocupada, indeseable */
     pthread_ mutex_lock(&lock_de_mi_variable);
       if ( variable < MAXIMO)
             done=1;
      pthread_mutex_unlock(&lock_de_mi_variable);
  }
   /* hago lo que esperaba para variable < MAXIMO */
  :
En Otra hebra:
   :
   pthread_ mutex_lock(&lock_de_mi_variable);
   variable--;
   pthread_mutex_unlock(&lock_de_mi_variable);
   :

POSIX entrega una forma mejor de resolver este problema. La idea es que si la condición falla, en lugar de volver a consultar, quedarse bloqueado hasta que se nos informe de su cambio y así tenga sentido volver a consultar.  El recurso creado para esto se conoce como Variable de Condición. Éstas son representadas por el tipo de dato pthread_cond_t. Similar a mutex, podemos iniciar su valor con la constante PTHREAD_COND_INITIALIZER. En lugar de cargar los atributos por omisión, podemos usar el llamado a la función pthread_cond_init.

#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);

int pthread_cond_destroy(pthread_cond_t *restrict cond);

Ambas retornan 0 si es OK, y un número de error en caso de falla.

Usamos la función pthread_cond_wait para esperar por un cambio que dé sentido a evaluar nuevamente la condición. Existe una variante para limitar la espera a un cierto tiempo.

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

int pthread_cond_timewait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout);

Ambas retornan 0 si es OK, y un número de error en caso de falla.

Cuando un hilo invoca a pthread_cond_wait, éste libera el candado del mutex usado en el argumento. Más adelante cuando es despertado, éste candado es devuelto a la hebra.

Con estos llamados el problema se resuelve de la siguiente manera:
Visibles a ambas hebras:
  pthread_mutex_t    lockDeMiVariable = PTHREAD_MUTEX_INITIALIZER;
  pthread_cond_t     cambioDeVariable = PTHREAD_COND_INITIALIZER;
  int variable = 0;

En una hebra:
   :
  int done = 0;
  while (!done) {  
     pthread_ mutex_lock(&lockDeMiVariable);
       whilevariable >= MAXIMO )  /* Debo seguir esperando mi condición !*/
         pthread_cond_wait(&cambioDeVariable,  &lockDeMiVariable); /*espero bloqueado*/
       done=1;
      pthread_mutex_unlock(&lockDeMiVariable);
  }
  /* hago lo que esperaba para variable < MAXIMO */
  :
En Otra hebra:
   :
   pthread_ mutex_lock(&lockDeMiVariable);
   variable--;
   pthread_mutex_unlock(&lockDeMiVariable);
   pthread_cond_signal(&cambioDeVariable);
   :

Hay dos funciones para notificar a hebras que una condición ha cambiado. La función pthread_cond_signal  despertará a una hebra que espera con la misma variable de condición. La función pthread_cond_broadcast despierta a todas la hebras esperando por la condición.

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *restrict cond);

int pthread_cond_broadcast(pthread_cond_t *restrict cond);

Ambas retornan 0 si es OK, y un número de error en caso de falla.

Un ejemplo de uso de hebras y variables condicionales: Cree un programa que envía a pantalla todo lo que se ingrese por teclado. Para probar este programa y poder distinguir entre lo enviado por el programa y el eco automático del teclado, usted lo puede correr usando:
$ mkfifo myfifo
Luego en una consola pone:
$ cat myfifo
y en otra pone:
$ programaEco > myfifo

Ahora deseamos limitar a 1 caracter por segundo lo enviado a pantalla. Usaremos aquí el algoritmo "leaky bucket" con tamaño de balde de 10 caracteres. Usted puede ejecutar el programa como el previo y verá cómo los caracteres aparecen de a uno a la vez y con una tasa de 1 caracter/s.
Para controlar la tasa de tiempo, usted puede usar un timer iterativo o simplemente sleep(1). Usted puede estudiar ambas soluciones, ver:
leakyBucket_with_sleep.c: Solución simple que hace uso de sleep para controlar el goteo del balde.
leakyBucket.c :  Solución similar a la previa pero más precisa y compleja. Hace uso de SIGALRM, la cual requiere manejo de señales en hilos.