CodeGym /Cursos /C# SELF /Otras problemas: Livelock y Starvation

Otras problemas: Livelock y Starvation

C# SELF
Nivel 57, Lección 3
Disponible

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
2
Tarea
C# SELF, nivel 57, lección 3
Bloqueada
Demostración de Starvation
Demostración de Starvation
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION