← Concurrencia y Sincronización Bloque 4 · Mutex · Tema 17 de 47

Tema 17

Errores comunes con mutex

El mutex es simple, pero su mal uso es la causa de muchos bugs en programas concurrentes. Olvidar el unlock, bloquear dos veces el mismo mutex, o no manejar los errores de lock pueden causar deadlocks o condiciones de carrera. En este tema verás los errores más comunes, cómo evitarlos con trylock y timedlock, y los tipos especiales de mutex (errorcheck y recursivo).

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:

olvidar unlock
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:

patrón seguro
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:

patrón con goto
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):

double lock
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.

El doble lock es un deadlock inmediato

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:

trylock
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:

timedlock
#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:

TipoComportamientoUso típico
PTHREAD_MUTEX_NORMALBloquear dos veces causa deadlock. Desbloquear un mutex no bloqueado por el hilo es comportamiento indefinido.El más común. Rápido.
PTHREAD_MUTEX_ERRORCHECKDetecta errores: bloquear dos veces retorna EDEADLK, desbloquear sin bloquear retorna EPERM.Desarrollo y depuración.
PTHREAD_MUTEX_RECURSIVEPermite bloquear múltiples veces por el mismo hilo. Cada unlock decrementa el contador.Funciones recursivas que bloquean el mismo mutex.
PTHREAD_MUTEX_DEFAULTDepende 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:

errorcheck
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:

recursivo
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.

El mutex recursivo puede ocultar bugs

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:

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:

  1. Lock/unlock en el mismo ámbito: bloquear al inicio de una función y desbloquear al final. simple
  2. RAII (Resource Acquisition Is Initialization): en C++, usar un objeto que bloquea en el constructor y desbloquea en el destructor. C++
  3. Un solo punto de salida: usar goto para unificar el desbloqueo. goto
  4. 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:

patrón seguro
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 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

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

  1. Escribe un programa que olvide el unlock y observa el deadlock.
  2. Usa un mutex errorcheck y verifica que detecta el doble lock.
  3. Implementa trylock en un bucle con backoff.
  4. Implementa timedlock con un timeout de 1 segundo.
  5. Usa ThreadSanitizer para detectar un error con mutex.
  6. Investiga cómo Helgrind detecta deadlocks.