Sincronización de hilos en POSIX: Mutex


Objetivos: Conocer los mecanismos de sincronización para acceso controlado a objetos compartidos por hebras.

Funciones POSIX para Sincronización
   Estas funciones incluyen mecanismos de exclusión mutua (mutex), mecanismos de señalización del cumplimiento de condiciones por parte de variables, y mecanismos de acceso a variables que se modifican en forma exclusiva, pero pueden ser leídas en forma compartida (escritor único, múltiples lectores). Las funciones para el manejo de zonas de acceso exclusivo tienen el prefijo pthread_mutex
    Un mutex es una variable especial que puede tener estado abierto o libre (unlocked), o cerrado o tomado  (locked). Es como una compuerta que permite el acceso controlado. Si un hilo toma el mutex, entonces se dice que es el dueño del mutex. Si ningún hilo lo tiene se dice que está libre (o unclocked). Cada mutex tiene una cola de hilos que están esperando para tomar el mutex. El uso de mutex es eficiente, pero debería ser usado sólo cuando es solicitado por corto tiempo para no bloquear el acceso de otros hilos interesados en un recurso.

Función POSIX
Descripción
pthread_mutex_init(...) Permite dar las condiciones iniciales a un candado mutex
pthread_mutex_lock(...)
Permite solicitar acceso al mutex, el hilo se bloquea hasta su obtención
pthread_mutex_trylock(...)
permite solicitar acceso al mutex,  el hilo retorna inmediatamente. El valor retornado indica si otro hilo lo tiene.
pthread_mutex_unlock(...)
Permite liberar un mutex.
pthread_mutex_destroy(....) Destruye la variable (de tipo pthread_mutex_t) usada para manejo de exclusión mutua, o candado mutex

Creación e iniciación de mutex

#include <pthread.h>
Se crea definiendo una variable del tipo pthread_mutex_t, como en:
pthread_mutex_t    mutex;

Para dar su valor ininicial usamos:
int  pthread_mutex_init(pthread_mutex_t * mutex, const pthread_mutexattr_t * attr);

Alternativamente podemos invocar (mucho más simple):
pthread_mutex_t    mutex = PTHREAD_MUTEX_INITIALIZER;
lo cual crea e inicia el mutex con los atributos por omisión (default). Es el uso que daremos en este curso.
Es equivalente a invocar:
pthread_mutex_t mylock;
pthread_mutex_init(& mylock, NULL);

Destrucción de un mutex

#include <pthread.h>
int  pthread_mutex_destroy(pthread_mutex_t * mutex);

Si el mutex lo tenía otro hilo y éste es destruido, POSIX no define el comportamiento de mutex en esta situación.

Solicitud y liberación de un mutex

#include <pthread.h>
int  pthread_mutex_lock(pthread_mutex_t * mutex);
int  pthread_mutex_trylock(pthread_mutex_t * mutex);
int  pthread_mutex_unlock(pthread_mutex_t * mutex);

Con pthread_mutex_trylock el hilo siempre retorna, si la función es exitosa, se retorna 0 -como en los otros casos; si no se retornará EBUSY indicando que otro hilo tiene el mutex.

Ejemplo: Para proteger una zona crítica, usar:
pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mylock);
/* Sección crítica */
pthread_mutex_unlock(&mylock);

Ejemplo:
Sin Mutex Con Mutex

int counter=0;

/* Function C */
void functionC()
{

counter++

}
/* Note scope of variable and mutex are the same */
pthread_mutex_t myMutex = PTHREAD_MUTEX_INITIALIZER;
int counter=0;

/* Function C */
void functionC()
{
pthread_mutex_lock( &myMutex );
counter++
pthread_mutex_unlock( &myMutex );
}
Ejemplo: couter.c : conjunto de funciones para acceder a un contador a través de múltiples hilos. countertest.c, los archivos necesarios (wastetime.c) los puede bajar de aquí. Vea el Makefine para generar ejecutables.
Multiplicación de vectores sin threads y con threads.
Vea Dos hebras modifican un mismo valor global 1er certamen año 2009. Una hebra incrementa y otra decrementa un mismo valor global, con control de acceso exclusivo.
Vea pregunta 3 del primer certamen del año 2007.
Vea: pregunta 1er certamen 2012: en búsqueda de inconsistencia al camiar una variable compartida entre dos hebras (versión con String en lugar de long double).

Muchos llamados al sistema no son seguros de usar desde hilos (no son re-entrantes), por ejemplo, strtok, rand, localtime, etc... Si necesitamos usar alguna de estas funciones, debemos considerarlas dentro de un código del tipo:
#include <pthread.h>
#include <stdlib.h>

int randsafe(double *ranp) {
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int error= ERROR_CODE;

if (error = pthread_mutex_lock(&lock))
return error;
*ranp = (rand() + 0.5)/(RAND_MAX + 1.0);
return pthread_mutex_unlock(&lock);

}
Ejemplo: programa que calcula la suma de valores aleatorios de la función sin(), en varios hilos, los suma muestra su resultado.

Mecanismos de sincronización Reader-writer
  Estos mecanismos permiten acceso exclusivo para escritura pero accesos compartidos para lectura. También son conocidos como read-write locks. La variable usada para proveer la sincronización en este caso es de tipo: pthread_rwlock_t. Así tres estados son posibles para este candado: cerrado en modo lectura, cerrado en modo escritura, y no cerrado. Sólo un hilo puede tener el candado en modo escritura, pero múltiples hilos pueden tener el candado en modo lectura. Cuando un hilo es bloqueado a la espera de liberación del candado en modo lectura; es decir él quiere escribir, hilos subsiguientes deseando tomar el candado en modo lectura serán también bloqueados. Así se evita espera indefinida de hilos esperando escribir.
Las funciones son:

Función POSIX
Descripción
pthread_rwlock_init (...)
permite dar las condiciones iniciales a un candado de lectura y escritura.
pthread_rwlock_destroy(...)
Destruye la variable (de tipo pthread_rw_lock_t) usada para manejo de exclusión mutua, o candado read-write.
pthread_rwlock_wrlock(...)
Permite solicitar acceso de escritura al candado, el hilo se bloquea hasta su obtención
pthread_rwlock_rdlock(...)
permite solicitar acceso de lectura al candado,  el hilo accede si el candado está libre o si sólo ha sido concedido para lectura.
pthread_rwlock_unlock(...)
Permite liberar un candado read-write.
othread_rwlock_tryrdlock(...) Permite obtener el candado (lock) si está libre y retorna cero, en otro caso retorna EBUSY
pthread_rwlock_trywrlock(...) Permite obtener el candado (lock) si está libre y retorna cero, en otro caso retorna EBUSY

Evitando Deadlock
Como se estudia en Sistemas Operativos, la mala programación o mal uso de estos mecanismos de sincronización pueden conducir a Bloqueos Indefinidos (deadlock). Una forma simple de evitar esta situación es asegurar que cada hilo pida acceso a las zonas exclusivas siguiendo un orden común en todos los hilos. Si tenemos los candados a, b y c, todos los hilos deben pedir sus candados en ese orden. Otra opción es usar los tryxxlock, si es exitoso, OK; si no lo es, se deben liberar locks previos, e intentar nuevamente.