1. Introducción
Livelock — es una situación en una aplicación multihilo cuando dos o más hilos intentan evitar un bloqueo mutuo (Deadlock), pero al final solo reaccionan infinitamente a las acciones del otro, sin avanzar. A diferencia de Deadlock, donde los hilos están paralizados y no pueden hacer nada, en un Livelock el programa parece estar vivo: los hilos se ejecutan, pero ninguno hace trabajo útil.
Es como si tú y un colega en un pasillo estrecho dieran continuamente pasos atrás para dejar pasar al otro y por eso nunca avanzaran. Divertido desde fuera, pero terrible en un programa.
Ejemplo de Livelock: programadores en la puerta
Imagina a dos programadores muy educados que caminan el uno hacia el otro por un pasillo estrecho. Llegan a una puerta al mismo tiempo y ambos deciden ceder: se apartan para dejar pasar al otro. Ambos vuelven a ver que la situación no cambió y vuelven a ceder — infinitamente. Nadie está quieto, todos se mueven, pero nadie pasa la puerta.
En un programa multihilo esto se ve así: un hilo ve el recurso ocupado, retrocede, vuelve a comprobar, vuelve a ceder, y así hasta el infinito.
2. Livelock en la práctica
Vamos a implementar una situación análoga en código. Supongamos que tenemos dos recursos (por ejemplo, dos cuentas bancarias), y dos hilos intentan transferir dinero entre sí al mismo tiempo, tratando de no "quedarse atascados".
class Account
{
public int Balance { get; set; }
public object LockObj { get; } = new object();
}
public class Program
{
static void Transfer(Account from, Account to, int amount)
{
while (true)
{
bool lockedFrom = false;
bool lockedTo = false;
try
{
Monitor.TryEnter(from.LockObj, 100, ref lockedFrom);
Monitor.TryEnter(to.LockObj, 100, ref lockedTo);
if (lockedFrom && lockedTo)
{
from.Balance -= amount;
to.Balance += amount;
break; // ¡Transferencia realizada!
}
}
finally
{
if (lockedFrom) Monitor.Exit(from.LockObj);
if (lockedTo) Monitor.Exit(to.LockObj);
}
// ¿No salió? Retrocedemos y lo intentamos otra vez — ¡con educación!
Thread.Sleep(1);
}
}
// ... Lanzar dos hilos con Transfer(A, B, ...); Transfer(B, A, ...);
}
En este ejemplo ambos hilos intentan repetidamente tomar los locks. Pero si ambos ven siempre que los locks están ocupados y ceden, todo se reduce a ceder "educadamente" y no ocurre ninguna transferencia de dinero. Confiar en la aleatoriedad del planificador es una mala estrategia para un programa fiable. Añadid pausas y backoff, no hagáis ciclos cortos "vacíos" con Monitor.TryEnter y Thread.Sleep.
Comparación Livelock vs Deadlock
| Deadlock | Livelock | |
|---|---|---|
| Hilos | Parados, esperando | Activos dando vueltas, esperando |
| Recursos | Bloqueados para siempre | No están bloqueados explícitamente |
| CPU | Casi sin carga | PUEDE estar al 100% |
| Solución | Timeouts, orden de locks | Aleatoriedad, pausas, backoff |
3. Starvation (hambre): cuando una cola no llega al recurso
Starvation (literalmente "hambre") — es una situación cuando uno o varios hilos son constantemente ignorados y no obtienen acceso al recurso necesario, porque otros hilos siempre quedan por delante.
A diferencia de Deadlock y Livelock, aquí nadie está bloqueado para siempre y nadie cede infinitamente — simplemente algunos hilos constantemente no reciben su "pedazo de tarta".
Ilustración: analogía de la vida real
Imagina una cantina con dos colas: "VIP" (por ejemplo, caja para empleados del año) y "para el resto". La cola VIP es corta, pero siempre hay alguien que llega antes. Los visitantes "normales" esperan media hora viendo cómo los VIP vuelven a pasar por delante. ¡Eso es Starvation!
4. Starvation en C#: práctica y depuración
Veamos la situación con lock, cuando un hilo con prioridad alta constantemente entra en la sección crítica, y otro "se queda atrás":
private static readonly object _locker = new object();
static void Greedy()
{
while (true)
{
lock (_locker)
{
Console.WriteLine("Hilo codicioso ha tomado el recurso...");
Thread.Sleep(10); // mantiene el lock más tiempo
}
Thread.Sleep(1);
}
}
static void Poor()
{
while (true)
{
lock (_locker)
{
Console.WriteLine("Hilo pobre intentó entrar...");
}
}
}
Si lanzáis ambos hilos, "Greedy" ("Codicioso") mantiene el lock mucho tiempo y lo vuelve a tomar rápido, mientras que "Poor" ("Pobre") casi no entra y de hecho "padece hambre" sin el recurso. En tareas reales el starvation puede ser menos obvio: por ejemplo, si un hilo tiene prioridad baja o es desplazado más a menudo por el sistema operativo.
¿Dónde aparece el starvation?
- En hilos con prioridad baja.
- Con una organización incorrecta de colas de tareas (sin política justa FIFO).
- En ReaderWriterLockSlim con la configuración por defecto: si el escritor llega raramente, y los lectores van en flujo infinito, el escritor estará siempre "hambriento", porque los lectores ocupan el Read Lock y no permiten obtener el Write Lock.
5. Por qué Starvation no siempre es bug, pero siempre es un problema
Starvation, a diferencia de Deadlock, no provoca la parada completa del programa, pero el resultado del trabajo se vuelve impredecible e injusto: los datos se procesan de forma desigual, algunas tareas esperan eternamente, el rendimiento cae. Esto es especialmente crítico en aplicaciones servidor donde todos deben recibir las mismas oportunidades.
Cómo detectar y diagnosticar Livelock y Starvation
- El consumo de CPU ha crecido mucho, pero la tarea "no avanza" — posiblemente tienes un Livelock.
- Los hilos parecen trabajar, pero algunas tareas nunca terminan — probable Starvation.
- Logs: si el registro muestra que ciertos hilos o tareas nunca obtienen acceso al recurso — es starvation al 100%.
Cómo evitar Livelock
- No te limites a intentar capturar recursos solo con métodos non-blocking "concesivos" como Monitor.TryEnter. Si no funciona, añade pausas aleatorias (Thread.Sleep(Random.Next(1, 10)))), para que los hilos no coincidan en el tiempo.
- No uses ciclos cortos y constantes intentando tomar el recurso — de lo contrario el ciclo "me aparto — intento" será infinito.
- A veces ayuda cambiar el algoritmo: introducir "derecho del mayor" para que solo un hilo pueda ceder, y el otro no.
Cómo evitar Starvation
- Controla las prioridades de los hilos: tareas de fondo o de alta prioridad no deben impedir eternamente a las demás.
- Usa colas (ConcurrentQueue<T>, canales, colas de tareas) con política justa FIFO, para que las tareas se atiendan por orden.
- En ReaderWriterLockSlim puedes suspender nuevos lectores en favor de escritores, para que el escritor consiga finalmente el Write Lock.
- Aplica timeouts y logs: si un hilo intenta obtener un lock demasiado tiempo, emite una advertencia.
- Reduce la duración de mantener locks y distribuye las secciones críticas.
Comparación Deadlock, Livelock y Starvation
| Escenario | Deadlock | Livelock | Starvation |
|---|---|---|---|
| Hilos | Parados muertos | Corren, pero en vano | Algunos trabajan, otros no |
| Acceso al recurso | No | No | Desigual, a veces no |
| CPU | No cargada | Cargada fuertemente | Cargada, pero no por todos |
| Error crítico? | Sí | Sí | Algunas veces |
GO TO FULL VERSION