CodeGym /Cursos /C# SELF /Bloqueos mutuos: Deadlocks

Bloqueos mutuos: Deadlocks

C# SELF
Nivel 57 , Lección 1
Disponible

1. Ejemplo de Deadlock en C#

Deadlock — es la situación cuando cada hilo de un grupo toma un recurso y espera a que otro hilo del mismo grupo libere otro recurso — y nadie llega a nada.

Vamos a verlo en la práctica. Supongamos que tenemos dos objetos-lock y dos hilos. Cada hilo bloquea un objeto y luego intenta capturar el segundo.

using System;
using System.Threading;

class Program
{
    static readonly object lockerA = new object();
    static readonly object lockerB = new object();

    static void Main()
    {
        Thread thread1 = new Thread(Thread1Work);
        Thread thread2 = new Thread(Thread2Work);

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Console.WriteLine("Ambos hilos terminaron (si no ocurrió un Deadlock)");
    }

    static void Thread1Work()
    {
        lock (lockerA)
        {
            Console.WriteLine("Hilo 1: adquirió lockerA");
            Thread.Sleep(100); // Damos oportunidad al segundo hilo de adquirir lockerB

            lock (lockerB)
            {
                Console.WriteLine("Hilo 1: adquirió lockerB");
            }
        }
    }

    static void Thread2Work()
    {
        lock (lockerB)
        {
            Console.WriteLine("Hilo 2: adquirió lockerB");
            Thread.Sleep(100); // Damos oportunidad al primer hilo de adquirir lockerA

            lock (lockerA)
            {
                Console.WriteLine("Hilo 2: adquirió lockerA");
            }
        }
    }
}

Ejecuta este código varias veces — y casi con seguridad te encontrarás con que la aplicación "se queda colgada". ¡Ocurrió un deadlock! Cada hilo tomó su lock y ahora espera a que el otro libere el segundo.

Visualización del Deadlock

Este es el esquema de cómo se ve:

sequenceDiagram
    participant Hilo1
    participant Hilo2
    participant lockerA
    participant lockerB

    Hilo1->>lockerA: adquirir lockerA
    Hilo2->>lockerB: adquirir lockerB
    Hilo1->>lockerB: espera a que se libere lockerB
    Hilo2->>lockerA: espera a que se libere lockerA

Como resultado: Hilo 1 mantiene lockerA y espera lockerB, Hilo 2 mantiene lockerB y espera lockerA. Nadie cede — el programa se queda colgado.

2. ¿Cómo detectar un Deadlock?

¿Por qué ocurre esto?

Para entender la causa, miremos algunos componentes:

  • Bloqueo múltiple: Cuando un hilo adquiere varios locks a la vez.
  • Orden de bloqueos incorrecto: Si los hilos adquieren locks en distinto orden, puede surgir un deadlock.
  • Posibilidad de espera: El hilo espera a que se libere un recurso que está ocupado por otro hilo.
  • Sin fuerza para arrancar el lock: Un hilo no puede "arrebatar" un lock a otro en C# — solo puede esperar.

La clásica "trampa": si tienes N hilos y N recursos, y los hilos los bloquean en distinto orden, estás a las puertas de un deadlock.

Señales típicas de Deadlock

Desde fuera, un deadlock suele parecer simplemente que el programa "se quedó colgado". Los hilos están vivos, pero en espera de recursos, esperando eternamente el uno al otro.

Señales típicas:

  • El programa dejó de responder inesperadamente (a veces solo bajo cierta carga).
  • El debugger muestra que los hilos están atascados en lock, Monitor.Enter, WaitOne, EnterReadLock u operaciones similares de sincronización.
  • Las herramientas del sistema (por ejemplo, Task Manager o las herramientas de Rider/Visual Studio) muestran 0% de uso de CPU — "todo parado".
  • En los logs — no hay errores, pero tampoco actividad.

Si usas JetBrains Rider o Visual Studio, mira con el debugger dónde están atascados tus hilos (Stack Trace). Si ves que varios hilos están en lock/Mutex.WaitOne esperando unos por otros — felicitaciones (o más bien, lo siento): eso es un deadlock.

Escenarios clásicos donde aparece Deadlock

Orden de bloqueos descoordinado. Como en nuestro ejemplo de arriba: Hilo 1 bloquea lockerA, luego lockerB; Hilo 2 — al revés. Listo, la trampa está puesta.

Locks anidados. Cuando dentro de un bloque lock haces otro lock sobre otro objeto.

Recursos que se cruzan en métodos. Es especialmente difícil de rastrear si los locks están dispersos por distintos métodos/clases y aparecen en distintas combinaciones. Los escenarios se complican cuando hay más de dos locks.

3. Prevención del Deadlock

La buena noticia: ¡se puede proteger contra deadlocks!
La mala noticia: hay que ser más cuidadoso al diseñar código multihilo.

Aquí tienes varias recomendaciones prácticas (apréndetelas — en una entrevista te sumarán puntos):

Siempre adquiere locks en el mismo orden

Si tienes varios objetos que pueden bloquearse juntos — define un orden común (por ejemplo, por nombre o índice) y síguelo siempre.

Ejemplo — adquirimos locks correctamente

static void SafeLock(object objA, object objB)
{
    // Comparamos hashes — para que los hilos siempre bloqueen los objetos en el mismo orden
    object first = objA.GetHashCode() < objB.GetHashCode() ? objA : objB;
    object second = objA.GetHashCode() < objB.GetHashCode() ? objB : objA;

    lock (first)
    {
        lock (second)
        {
            // sección crítica
        }
    }
}

Aquí — ambos hilos entrarán primero en lock(objA), luego en lock(objB), si objA es "menor" que objB.

Usa timeouts

Si un hilo no puede adquirir un lock en un tiempo razonable — que lance una excepción o salga correctamente. Sirve Monitor.TryEnter.

Ejemplo con Monitor.TryEnter

bool lockTakenA = false;
bool lockTakenB = false;

try
{
    Monitor.TryEnter(lockerA, 500, ref lockTakenA);
    if (!lockTakenA)
    {
        // No se pudo obtener el lock en 500 ms — nos rendimos sin deadlock
        return;
    }

    Monitor.TryEnter(lockerB, 500, ref lockTakenB);
    if (!lockTakenB)
    {
        return;
    }

    // Sección crítica...

}
finally
{
    if (lockTakenB)
        Monitor.Exit(lockerB);
    if (lockTakenA)
        Monitor.Exit(lockerA);
}

Si no lograste obtener el lock — ¡no te quedes colgado!

Minimiza el código dentro de la sección crítica

Mantén en las secciones críticas solo el código estrictamente necesario.

No mezcles distintos mecanismos de sincronización

Si usas a la vez lock, Mutex, Semaphore, ReaderWriterLockSlim — aumenta el riesgo de enredarte y provocar un deadlock.

4. Deadlock con Mutex, Semaphore y ReaderWriterLockSlim

Los deadlocks pueden ocurrir no solo con el clásico lock o Monitor, sino con otros mecanismos de sincronización.

Deadlock con Mutex

static Mutex mutexA = new Mutex();
static Mutex mutexB = new Mutex();

void Work1()
{
    mutexA.WaitOne();
    Thread.Sleep(100);
    mutexB.WaitOne();

    // Sección crítica

    mutexB.ReleaseMutex();
    mutexA.ReleaseMutex();
}

Si hay un segundo hilo que los adquiere en orden inverso — obtendrás un deadlock.

Deadlock con ReaderWriterLockSlim

ReaderWriterLockSlim, a pesar de su flexibilidad, no protege contra deadlocks si un hilo ya ha adquirido un tipo de lock y luego intenta obtener otro.

Por ejemplo, si un hilo mantiene un read-lock (EnterReadLock) y intenta entrar en write-lock (EnterWriteLock) — el deadlock está asegurado, porque el write-lock no puede obtenerse mientras no se liberen todos los read-locks.

5. Cómo evitar Deadlock en aplicaciones reales

Veamos cómo puede verse esto en una app de demostración. Supongamos que desarrollamos un simulador de tienda online, donde trabajan a la vez:

  • Hilos que actualizan el stock (writers)
  • Hilos que verifican disponibilidad de producto (readers)
  • Administrador (hilo especial) que tanto lee como escribe

Mal:

lock (stockLock)
{
    // actualización de stock
    lock (userLock)
    {
        // algo con usuarios
    }
}
// ... ¡y en otro lado al revés!
lock (userLock)
{
    lock (stockLock) { }
}

Bien:

Ponte de acuerdo — siempre intentamos primero bloquear stockLock, luego userLock en todos los hilos.

6. Cómo no conseguir un Deadlock en el trabajo y en entrevistas

En proyectos reales:

  • Al diseñar — dibuja el esquema de hilos y locks (puede ser en papel/pizarra).
  • Comparte el acuerdo de orden de bloqueos con todo el equipo.
  • Usa herramientas de análisis estático (Rider/Visual Studio, ReSharper) — pueden encontrar deadlocks potenciales.
  • Lee artículos en Microsoft Docs sobre Deadlock.

En entrevistas:

  • Muestra que entiendes el problema y sus síntomas clásicos.
  • Menciona las principales formas de protección: orden único, timeouts (TryEnter), secciones críticas mínimas.
  • Da el ejemplo con TryEnter y explica por qué es importante liberar todos los recursos adquiridos en el finally.

7. Errores típicos y deadlocks inesperados

Error Nº1: locks no evidentes dentro de librerías de terceros.
Clásico — logging dentro de la sección crítica. El hilo se queda atascado y ni te imaginas que es culpa de código ajeno.

Error Nº2: relock del mismo recurso.
Esto es un reentrant deadlock: el hilo intenta entrar en un lock que ya posee, pero el mecanismo no soporta reentrada.

Error Nº3: usar métodos async dentro de secciones críticas.
Especialmente peligroso con await: el hilo puede "ceder" el control en el peor momento y bloquear todo el sistema.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION