1. Introducción
Imagínate que un hilo es un empleado incansable a quien le asignas trabajo. El empleado puede estar durmiendo (aún no empezó a trabajar), currando a tope (se está ejecutando tu método), esperando a que le des una nueva tarea (idle), o terminar su jornada (finalizar).
En C# (y en .NET en general) el ciclo de vida de un hilo consta de varios estados:
- Unstarted — el hilo está creado, pero todavía no arrancó.
- Running — el hilo se está ejecutando.
- WaitSleepJoin — el hilo está temporalmente inactivo (por ejemplo, esperando una señal o "durmiendo").
- Stopped — el hilo completó la tarea y terminó.
Puedes visualizar este ciclo con este esquema:
stateDiagram-v2
[*] --> Unstarted
Unstarted --> Running: Start()
Running --> WaitSleepJoin: Wait/Sleep/Join
WaitSleepJoin --> Running: Señal recibida/Tiempo agotado
Running --> Stopped: Método finalizado
WaitSleepJoin --> Stopped: Método finalizado
Stopped --> [*]
Todo empieza con la creación del objeto Thread, pero hasta que no llames a Start() el hilo "duerme" en el estado Unstarted. Tras llamar a Start() — empieza la fiesta, el hilo pasa a Running. Si el hilo dentro del código ejecuta Thread.Sleep o espera algo (por ejemplo, Monitor.Wait), entrará en un estado especial de espera. En cuanto el método pasado al hilo finaliza, el hilo muere, deja de existir y no "resucita". Es un viaje de ida.
2. Práctica: ciclo de vida de un hilo simple
Veamos un ejemplo clásico:
using System;
using System.Threading;
class Program
{
static void Main()
{
// Creamos el hilo — por ahora solo planificamos el trabajo
Thread worker = new Thread(DoWork);
Console.WriteLine($"Estado del hilo después de crear: {worker.ThreadState}");
// Arrancamos el hilo
worker.Start();
Console.WriteLine($"Estado del hilo después de arrancar: {worker.ThreadState}");
// Dormimos el hilo principal un poco para que el hilo worker tenga tiempo de currar
Thread.Sleep(100);
Console.WriteLine($"Estado del hilo (más tarde): {worker.ThreadState}");
// Esperamos a que worker termine (nos unimos)
worker.Join();
Console.WriteLine($"Estado del hilo después de terminar: {worker.ThreadState}");
Console.WriteLine("El hilo principal ha terminado");
}
static void DoWork()
{
Console.WriteLine("¡El hilo de trabajo ha empezado a trabajar!");
Thread.Sleep(500);
Console.WriteLine("¡El hilo de trabajo ha terminado!");
}
}
¿Qué muestra el programa?
- Después de crear el hilo — el estado será Unstarted.
- Tras arrancarlo — normalmente será inmediatamente Running (pero puede aparecer como Running | Background).
- Mientras trabaja — el estado puede ser Running, o WaitSleepJoin si el hilo está "durmiendo".
- Al terminar el método — el estado pasa a Stopped.
Este código es una gran herramienta para entender en qué estado puede estar tu hilo. Puedes jugar con los delays y ver cómo cambia el estado.
3. Gestión del hilo: métodos principales
Arranque: Start()
Obvio, pero lo repetimos: creas un hilo — arráncalo con Start(). Y ojo, se puede arrancar solo una vez: intentar llamar a Start() de nuevo lanzará una excepción ThreadStateException.
Thread t = new Thread(MyMethod);
t.Start(); // OK
t.Start(); // ¡Error!
Esperar a que termine: Join()
A veces necesitas esperar a que un hilo acabe todo y luego continuar. Para eso está Join().
Thread t = new Thread(MyMethod);
t.Start();
t.Join(); // Bloquea el hilo actual hasta que t termine
Si tienes varios hilos, puedes llamar Join() para cada uno — el hilo principal esperará hasta que todos los "currantes" terminen.
Variantes: existe la sobrecarga Join(int millisecondsTimeout), que espera solo el tiempo indicado y luego continúa.
// Esperamos como máximo 2 segundos
if (t.Join(2000))
Console.WriteLine("El hilo terminó a tiempo");
else
Console.WriteLine("Ya estamos hartos de esperar...");
Parada forzada: por qué es mala idea
En versiones antiguas de .NET existía el método Thread.Abort(), que permitía "matar" un hilo al vuelo. Hoy en día casi no se usa — es peligroso y puede dejar la aplicación en un estado raro. La filosofía de .NET es: el hilo debe terminar voluntariamente. No "matas" al empleado — le das a entender educadamente que la jornada ha acabado.
4. Cómo "parar" correctamente un hilo
La forma más correcta y segura de detener el trabajo de un hilo es usar una bandera de cancelación o un indicador de finalización que el propio hilo compruebe periódicamente.
class Worker
{
private volatile bool shouldStop = false;
public void DoWork()
{
while (!shouldStop)
{
Console.WriteLine("¡Estoy trabajando!");
Thread.Sleep(300);
}
Console.WriteLine("El hilo termina su trabajo por petición.");
}
public void RequestStop()
{
shouldStop = true;
}
}
Uso:
Worker w = new Worker();
Thread t = new Thread(w.DoWork);
t.Start();
// Esperamos un poco
Thread.Sleep(1000);
// Pedimos al hilo que termine
w.RequestStop();
t.Join(); // Esperamos a que el hilo termine
Punto importante: volatile
La palabra clave volatile le dice al compilador y al procesador: "¡No caches este campo, toma siempre el valor más reciente!" Esto es importante para que el hilo vea el indicador de parada actualizado. Sin esto (o sin otros mecanismos de sincronización) el hilo podría no ver nunca tus cambios.
5. Transición de hilos a estados de espera y sueño
A veces un hilo deja de trabajar temporalmente — está o esperando o durmiendo.
Sueño: Thread.Sleep
Cuando quieres dar un respiro al hilo o ralentizar la ejecución (por ejemplo, para no saturar la CPU), usa Thread.Sleep(milliseconds).
// El hilo duerme 2 segundos
Thread.Sleep(2000);
Mientras duerme, el hilo no hace trabajo alguno.
Espera / Join
Cuando el hilo principal espera a que uno hijo termine (Join), el principal está "en pausa". De forma similar, si un hilo espera la liberación de un recurso (por ejemplo, con monitores u otros primitivos de sincronización), entra en un estado especial de espera.
6. Gestión de si un hilo es background
En .NET los hilos son de dos tipos: foreground (primer plano) y background (segundo plano). La diferencia es simple:
- Si en el proceso solo quedan hilos background, el proceso terminará automáticamente.
- El hilo principal y todos los hilos foreground deben terminar para que el proceso se detenga.
Puedes indicar explícitamente que un hilo sea background:
Thread t = new Thread(SomeMethod);
t.IsBackground = true; // Marcado como background
t.Start();
Ejemplo práctico — Demonio vs. hilo normal
Thread t = new Thread(() =>
{
while (true)
{
Console.WriteLine("¡Soy un fantasma (background), no me vas a parar!");
Thread.Sleep(500);
}
});
t.IsBackground = true; // Lo hacemos background
t.Start();
Thread.Sleep(1200);
Console.WriteLine("El hilo principal termina");
// Al acabar Main — el proceso muere, y nuestro hilo eterno también desaparece
Al finalizar Main — el proceso se termina; los hilos background se detienen automáticamente.
7. Detalles útiles
Qué no hacer con los hilos
- No reinventes el hilo. El objeto Thread vive una sola vez: cuando su método termina — el hilo murió, y intentar llamar a Start() otra vez lanzará una excepción.
- No intentes detener forzosamente hilos ajenos con métodos como Thread.Abort() o Thread.Suspend() — están obsoletos y son peligrosos.
- No ignores el cierre correcto del trabajo del hilo. Si un hilo usa archivos o recursos, asegúrate de liberarlos correctamente antes de que el hilo termine.
Comprobar estado y gestionar el ciclo de vida
if (t.IsAlive)
{
Console.WriteLine("El hilo sigue vivo");
}
else
{
Console.WriteLine("El hilo ha terminado");
}
IsAlive — es true mientras el hilo ejecuta su método; tras finalizar — es false.
Ciclo de vida de un hilo simple en .NET
| Estado | Cómo entrar | ¿Qué significa? | Cómo salir |
|---|---|---|---|
| Unstarted | |
Hilo creado, no arrancado | Llamar a Start() |
| Running | |
El hilo está ejecutando trabajo | Finalizar el método |
| WaitSleepJoin | Sleep(), Join(), espera | El hilo está temporalmente inactivo | Se termina la espera |
| Stopped | Método del hilo finalizado | El hilo "murió" | No sale — fin |
Preguntas frecuentes sobre la gestión del ciclo de vida del hilo
Pregunta: ¿Se puede matar un hilo por comando?
Respuesta: No y no deberías; los hilos deben vigilar por sí mismos el fin de la ejecución. Usa banderas de cancelación.
Pregunta: ¿Se puede reutilizar el objeto Thread?
Respuesta: No. Crea un nuevo objeto para cada trabajo nuevo.
Pregunta: ¿Qué pasa si el hilo principal termina y el hijo sigue trabajando?
Respuesta: Si el hilo hijo es background (IsBackground == true), la aplicación terminará. Si no — el proceso seguirá vivo hasta que todos los hilos terminen.
Pregunta: ¿Cómo limpiar recursos correctamente si un hilo termina por cancelación?
Respuesta: Usa bloques try...finally dentro del método del hilo para asegurar la liberación de recursos en cualquier caso.
8. Errores típicos y cómo evitarlos al trabajar con hilos
Error nº1: reutilizar el mismo objeto Thread.
No puedes arrancar el mismo objeto de hilo más de una vez. Tras finalizar el hilo no se puede reiniciar — eso provocará una excepción.
Error nº2: liberar mal recursos externos desde un hilo.
Si el hilo trabaja con ficheros, red o recursos externos, asegúrate de cerrarlos y liberarlos correctamente. Se recomienda usar bloques finally o la construcción using para evitar fugas y bloqueos.
Error nº3: crear demasiados hilos.
Un número excesivo de hilos complica la depuración y puede reducir el rendimiento. Un hilo de más es tiempo perdido buscando y arreglando bugs inesperados.
GO TO FULL VERSION