Ir al contenido

Deadlock de un hilo, trylock y livelock

🎯 Al terminar podrás

  1. Explicar por qué un solo hilo puede hacer deadlock y qué cambia con un mutex ERRORCHECK o RECURSIVE.
  2. Usar pthread_mutex_trylock y decir qué condición de Coffman rompe.
  3. Distinguir deadlock, livelock y starvation por tres criterios: ¿ejecutan?, ¿alguien progresa?, ¿se resuelve solo?
  4. Proponer tres formas de evitar el livelock y elegir la más robusta.

Antes necesitas: Coffman · Mutex POSIX

  • Detective escribe tu hipótesis del bug antes de abrir el diagnóstico
  • Debate elige una alternativa para el escenario y defiéndela
  • Feynman explícalo en voz alta sin mirar y compara con la lista

Repaso rápido: semáforos POSIX (diap. 4–7)

Sección titulada «Repaso rápido: semáforos POSIX (diap. 4–7)»

Un semáforo es un contador especial para recursos compartidos (Dijkstra, 1965):

  • P() (wait, down): si el contador es 0, bloquea; si no, lo decrementa.
  • V() (signal, post, up): nunca bloquea. Despierta a uno si hay alguno esperando; si no, incrementa.

En POSIX (man 7 sem_overview) el valor nunca baja de 0. Hay semáforos con nombre (sem_open("/nombre"), para procesos sin parentesco) y sin nombre (sem_init). Todo el detalle está en Mutex POSIX y semáforos.

void f() {
mutex_lock(&lock);
mutex_lock(&lock); // ← el mismo hilo espera un lock que él mismo tiene
}

No hace falta un segundo hilo: el hilo espera a que él mismo suelte el lock, cosa que nunca va a pasar. Con un pthread_mutex_t NORMAL (el predeterminado de Linux), esto se cuelga para siempre. Con ERRORCHECK, el segundo lock retorna EDEADLK; con RECURSIVE, funciona (cuenta 2). Esto se probó en una ejecución de prueba: ver tipos de mutex.

Cuándo pasa en la vida real: una función pública toma el lock y llama a otra función pública que también lo toma. Las diapositivas mencionan dos soluciones (que el curso no profundiza):

  • Replicar el código, para que la función interna no tome el lock.
  • Función auxiliar no atómica: la versión pública toma el lock y llama a aux_function1(e), que asume que el lock ya está tomado. Las funciones internas solo llaman a las auxiliares.
void function1(Structure *e) {
mutex_lock(&e->lock);
aux_function1(e); // trabaja sin tomar el lock
mutex_unlock(&e->lock);
}

«Equivalente a pthread_mutex_lock(), salvo que si el mutex está tomado (por cualquier hilo, incluido el actual), la llamada retorna de inmediato.»

  • Retorna 0 si lo obtuvo, y EBUSY si estaba tomado. Nunca bloquea.
  • Es el equivalente de sem_trywait (retorna −1 con errno = EAGAIN), que se usa en el barbero dormilón.
  • Rompe la condición hold and wait de Coffman: si no puedo tomar el segundo recurso, no me quedo esperando con el primero.
void process1(void) { void process2(void) {
acquire_lock(&resource_1); acquire_lock(&resource_2);
while (try_lock(&resource_2) == FAIL) { while (try_lock(&resource_1) == FAIL) {
release_lock(&resource_1); release_lock(&resource_2);
wait_fixed_time(); wait_fixed_time();
acquire_lock(&resource_1); acquire_lock(&resource_2);
} }
use_both_resources(); use_both_resources();
release_lock(&resource_2); release_lock(&resource_1);
release_lock(&resource_1); release_lock(&resource_2);
} }

La secuencia de las diapositivas:

  1. P1 toma el recurso 1 y P2 toma el recurso 2.
  2. Cada uno intenta el otro lock y falla. Por cortesía, suelta el que tenía, espera un tiempo fijo y lo vuelve a tomar.
  3. Como esperan lo mismo, vuelven a chocar exactamente igual… hasta que algo externo rompa la simetría.

«Ningún proceso está bloqueado, así que no es un deadlock. Pero no hay progreso posible: es algo equivalente, un livelock.»

DeadlockLivelockStarvation
¿Los procesos ejecutan?no: bloqueadossí: gastan CPUsí (otros avanzan)
¿Alguien progresa?nadienadiesí, todos menos la víctima
¿Se resuelve solo?nuncapuede, si cambian los tiempospuede, si cambian los tiempos
Ejemplolos 5 filósofos con un tenedor cada unodos procesos educados con trylock y espera fijafilósofo 0 en tanen-5-1.c

Programé el ejemplo con pthread_mutex_trylock: los dos hilos arrancan juntos con una barrera, cada uno espera 1 ms con el primer lock tomado, y luego intenta el segundo. Ejecución de prueba, 5 rondas:

== espera fija (1 ms)
ronda 0: reintentos P1=0 P2=1
ronda 1: reintentos P1=1 P2=0
ronda 2: reintentos P1=0 P2=1
…
== espera aleatoria (0.1–2 ms)
ronda 0: reintentos P1=1 P2=0
…

El livelock no se sostuvo: en todas las rondas, uno de los dos tuvo que reintentar una sola vez. En una máquina real, los tiempos nunca son idénticos: el planificador, las interrupciones y la caché meten ruido que rompe la simetría. Pero eso no prueba que el código sea correcto. El livelock es posible, y en un sistema con temporización muy regular (por ejemplo, un sistema embebido sin otras tareas) puede durar mucho.

Cómo evitarlo de verdad:

  1. Orden global de los locks: todos toman primero el recurso 1 y luego el 2. Rompe la espera circular y no hace falta trylock. Es la mejor solución.
  2. Espera aleatoria (randomized backoff), como el retroceso exponencial de Ethernet: hace muy improbable repetir el choque.
  3. Asimetría: un proceso cede siempre y el otro nunca. Es la idea de los filósofos asimétricos.
Pregunta de análisis: ¿el código con trylock puede tener deadlock? ¿y starvation?
  • Deadlock: no. Ningún proceso espera bloqueado sosteniendo un recurso: cuando try_lock falla, suelta lo que tiene. Se rompe hold and wait.
  • Livelock: sí, si ambos repiten la secuencia sincronizados (espera fija e igual).
  • Starvation: sí. Aunque no haya livelock, un proceso puede tener mala suerte siempre: el otro vuelve a tomar su lock justo antes de cada try_lock. Nada garantiza un orden.

Autoevaluación

0/5 correctas · 0 respondidas
1. Un hilo hace lock dos veces sobre el mismo mutex NORMAL…
2. pthread_mutex_trylock sobre un mutex tomado por el mismo hilo…
3. trylock + soltar y reintentar rompe la condición de Coffman…
4. En un livelock, los procesos…
5. La solución más robusta al ejemplo de las diapositivas es…

📈 ¿Qué tan bien lo dominas?

Elige pensando en lo que hiciste, no en lo que leíste. Para marcar 3 tienes que haber resuelto un ejercicio nuevo sin pistas y haberlo justificado. Si solo lo leíste y sientes que «ya entendí», todavía es un 1.

Fuente ·sem_9/11-OSL-Deadlock.pdf (diap. 4–14) · man pthread_mutex_trylock(3), sem_overview(7) · Prueba propia: livelock.c (ejecución de prueba)