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.
GO TO FULL VERSION