1. Olvidar el unlock
El error más común con mutex es olvidar liberar el lock. Esto ocurre cuando un hilo adquiere un mutex y no lo libera, por ejemplo, por un return anticipado o una condición de error:
void procesar(int *dato) {
pthread_mutex_lock(&mutex);
if (*dato < 0) {
return; /* BUG: se olvidó el unlock */
}
*dato *= 2;
pthread_mutex_unlock(&mutex);
}
Si *dato < 0, la función retorna sin liberar el mutex. Los demás hilos que intenten adquirirlo se bloquearán para siempre (deadlock).
La solución es usar un patrón que garantice el unlock en todos los caminos:
void procesar(int *dato) {
pthread_mutex_lock(&mutex);
if (*dato < 0) {
pthread_mutex_unlock(&mutex); /* liberar antes de retornar */
return;
}
*dato *= 2;
pthread_mutex_unlock(&mutex);
}
O mejor aún, usar un solo punto de salida:
void procesar(int *dato) {
int resultado = 0;
pthread_mutex_lock(&mutex);
if (*dato < 0) {
resultado = -1;
goto salir;
}
*dato *= 2;
salir:
pthread_mutex_unlock(&mutex);
return resultado;
}
2. Bloquear dos veces
Bloquear un mutex que ya está bloqueado por el mismo hilo causa un deadlock (con mutex normal):
void funcion_a(void) {
pthread_mutex_lock(&mutex);
funcion_b(); /* funcion_b también bloquea mutex */
pthread_mutex_unlock(&mutex);
}
void funcion_b(void) {
pthread_mutex_lock(&mutex); /* DEADLOCK: mutex ya está bloqueado */
/* ... */
pthread_mutex_unlock(&mutex);
}
Este error es común cuando una función que bloquea un mutex llama a otra que también lo bloquea. La solución es usar un mutex recursivo (que permite bloquear múltiples veces por el mismo hilo) o reestructurar el código para evitar el doble bloqueo.
Con un mutex normal, bloquear dos veces el mismo mutex por el mismo hilo causa un deadlock inmediato. El hilo se bloquea esperando un mutex que él mismo tiene.
3. trylock
pthread_mutex_trylock intenta adquirir el mutex sin bloquear. Si el mutex está libre, lo adquiere y retorna 0. Si está ocupado, retorna EBUSY inmediatamente:
int ret = pthread_mutex_trylock(&mutex);
if (ret == 0) {
/* Se adquirió el mutex. */
/* ... sección crítica ... */
pthread_mutex_unlock(&mutex);
} else if (ret == EBUSY) {
/* El mutex está ocupado. Hacer otra cosa. */
hacer_trabajo_alternativo();
} else {
/* Error. */
perror("pthread_mutex_trylock");
}
trylock es útil cuando el hilo tiene trabajo alternativo que puede hacer mientras espera, o cuando se quiere evitar el bloqueo (por ejemplo, en un bucle de eventos).
4. timedlock
pthread_mutex_timedlock intenta adquirir el mutex con un timeout. Si no se puede adquirir en el tiempo especificado, retorna ETIMEDOUT:
#include <time.h>
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 5; /* timeout de 5 segundos */
int ret = pthread_mutex_timedlock(&mutex, &ts);
if (ret == 0) {
/* Se adquirió el mutex. */
pthread_mutex_unlock(&mutex);
} else if (ret == ETIMEDOUT) {
/* Timeout: no se pudo adquirir en 5 segundos. */
printf("Timeout esperando el mutex\n");
} else {
perror("pthread_mutex_timedlock");
}
timedlock es útil cuando se quiere evitar esperas indefinidas: si el mutex no se puede adquirir en un tiempo razonable, el hilo puede tomar una acción alternativa (como reportar un error o reintentar).
5. Tipos de mutex
pthread ofrece varios tipos de mutex, configurables con pthread_mutexattr_settype:
| Tipo | Comportamiento | Uso típico |
|---|---|---|
| PTHREAD_MUTEX_NORMAL | Bloquear dos veces causa deadlock. Desbloquear un mutex no bloqueado por el hilo es comportamiento indefinido. | El más común. Rápido. |
| PTHREAD_MUTEX_ERRORCHECK | Detecta errores: bloquear dos veces retorna EDEADLK, desbloquear sin bloquear retorna EPERM. | Desarrollo y depuración. |
| PTHREAD_MUTEX_RECURSIVE | Permite bloquear múltiples veces por el mismo hilo. Cada unlock decrementa el contador. | Funciones recursivas que bloquean el mismo mutex. |
| PTHREAD_MUTEX_DEFAULT | Depende de la implementación (normalmente NORMAL). | El default. |
6. Mutex errorcheck
El mutex errorcheck detecta errores comunes y retorna un código de error en lugar de causar un deadlock:
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
pthread_mutex_init(&mutex, &attr);
/* Bloquear dos veces: retorna EDEADLK en lugar de deadlock. */
int ret = pthread_mutex_lock(&mutex);
if (ret != 0) {
/* Error. */
}
ret = pthread_mutex_lock(&mutex); /* EDEADLK */
if (ret == EDEADLK) {
printf("Deadlock detectado!\n");
}
/* Desbloquear sin haber bloqueado: retorna EPERM. */
pthread_mutex_unlock(&mutex); /* EPERM */
El mutex errorcheck es más lento que el normal (por la verificación adicional), pero es invaluable para detectar errores durante el desarrollo.
7. Mutex recursivo
El mutex recursivo permite que el mismo hilo bloquee el mutex múltiples veces. Cada lock incrementa un contador; cada unlock lo decrementa. El mutex se libera cuando el contador llega a 0:
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex, &attr);
void funcion_a(void) {
pthread_mutex_lock(&mutex); /* contador = 1 */
funcion_b();
pthread_mutex_unlock(&mutex); /* contador = 0, se libera */
}
void funcion_b(void) {
pthread_mutex_lock(&mutex); /* contador = 2 */
/* ... */
pthread_mutex_unlock(&mutex); /* contador = 1 */
}
El mutex recursivo es útil para funciones recursivas que bloquean el mismo mutex. Pero puede ocultar errores de diseño: si una función bloquea y otra libera, el contador puede quedar en un estado inconsistente.
Si una función bloquea el mutex y otra (diferente) lo libera, el contador queda inconsistente. El mutex recursivo no detecta este error. Usarlo con cuidado.
8. Mutex normal (default)
El mutex normal (PTHREAD_MUTEX_NORMAL) es el más rápido pero el menos seguro:
- Bloquear dos veces: deadlock inmediato.
- Desbloquear sin haber bloqueado: comportamiento indefinido (puede corromper el estado del mutex).
- Desbloquear desde otro hilo: comportamiento indefinido.
El mutex normal es el que se usa en la práctica (es el default), pero requiere disciplina del programador: siempre bloquear y desbloquear en el mismo hilo, y nunca bloquear dos veces.
9. Patrones seguros
Los patrones seguros para usar mutex son:
- Lock/unlock en el mismo ámbito: bloquear al inicio de una función y desbloquear al final. simple
- RAII (Resource Acquisition Is Initialization): en C++, usar un objeto que bloquea en el constructor y desbloquea en el destructor. C++
- Un solo punto de salida: usar goto para unificar el desbloqueo. goto
- trylock con reintento: usar trylock en un bucle con backoff. trylock
El patrón más seguro en C es el de un solo punto de salida:
int procesar(int *dato) {
int resultado = 0;
pthread_mutex_lock(&mutex);
if (*dato < 0) {
resultado = -1;
goto salir;
}
*dato *= 2;
salir:
pthread_mutex_unlock(&mutex);
return resultado;
}
10. Herramientas de detección
Las herramientas para detectar errores con mutex son:
- ThreadSanitizer: detecta carreras de datos y mal uso de mutex (bloqueo dos veces, desbloqueo sin bloquear).
- Helgrind: detecta mal uso de mutex y condiciones de carrera.
- Valgrind --tool=drd: más rápido que Helgrind, detecta carreras.
- Clang Static Analyzer: análisis estático que detecta posibles deadlocks.
ThreadSanitizer es la herramienta más usada en la práctica. Se compila con -fsanitize=thread y detecta errores en tiempo de ejecución.
11. Simulador interactivo
Este simulador muestra los errores comunes con mutex y cómo detectarlos:
Simulador · errores con mutex
12. Resumen
- Olvidar el unlock es el error más común: causa deadlock.
- Bloquear dos veces un mutex normal causa deadlock inmediato.
- trylock intenta adquirir sin bloquear; timedlock con timeout.
- Los tipos de mutex son: NORMAL, ERRORCHECK, RECURSIVE, DEFAULT.
- ERRORCHECK detecta errores en tiempo de ejecución.
- RECURSIVE permite bloquear múltiples veces por el mismo hilo.
- Los patrones seguros garantizan que cada lock tenga su unlock.
- ThreadSanitizer y Helgrind detectan errores con mutex.
13. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Cuál es el error más común con mutex?
Olvidar el unlock.
2. ¿Qué ocurre si un hilo bloquea un mutex dos veces (tipo NORMAL)?
Deadlock inmediato.
3. ¿Qué es trylock?
Intenta adquirir el mutex sin bloquear. Devuelve EBUSY si está ocupado.
4. ¿Qué es timedlock?
Intenta adquirir el mutex con un timeout. Devuelve ETIMEDOUT si no se puede adquirir en el tiempo.
5. ¿Qué detecta el mutex errorcheck?
Bloquear dos veces (EDEADLK) y desbloquear sin haber bloqueado (EPERM).
6. ¿Qué es un mutex recursivo?
Un mutex que permite bloquear múltiples veces por el mismo hilo.
7. ¿Cuál es el peligro del mutex recursivo?
Puede ocultar bugs si una función bloquea y otra (diferente) libera.
8. ¿Qué herramienta detecta errores con mutex?
ThreadSanitizer (con -fsanitize=thread) y Helgrind.
9. ¿Cuál es el patrón más seguro para usar mutex en C?
Un solo punto de salida con goto para unificar el desbloqueo.
10. ¿Qué syscall usa el mutex internamente?
futex (FUTEX_WAIT y FUTEX_WAKE).
Para practicar
- Escribe un programa que olvide el unlock y observa el deadlock.
- Usa un mutex errorcheck y verifica que detecta el doble lock.
- Implementa trylock en un bucle con backoff.
- Implementa timedlock con un timeout de 1 segundo.
- Usa ThreadSanitizer para detectar un error con mutex.
- Investiga cómo Helgrind detecta deadlocks.