1. Introducción
Si imaginas un proceso como un supermercado, los hilos (thread) son los cajeros en distintas cajas que atienden clientes simultáneamente. Todos los cajeros trabajan en la misma tienda, pero cada uno hace su tarea en paralelo, por eso el trabajo va más rápido y eficiente. Los hilos dentro de un proceso permiten ejecutar varias tareas a la vez, compartiendo recursos comunes y coordinando el trabajo dentro de una sola aplicación. La principal ventaja: los hilos pueden realmente ejecutarse en paralelo si el procesador soporta multitarea.
Necesidad práctica de hilos
¿Para qué arrancar varios hilos? Aquí tienes un par de situaciones reales:
- Estás escribiendo una aplicación con interfaz gráfica y no quieres que se "congele" durante una operación larga.
- Necesitas descargar varios archivos al mismo tiempo.
- En un juego, los enemigos deben pensar por su cuenta independientemente de los demás.
Curiosamente, en muchos programas todavía aparecen "congelamientos" por manejo inexperto de hilos. Hoy aprenderemos a evitar esos líos.
La clase Thread: la base del multihilo manual
La clase Thread es un dinosaurio dentro de la programación multihilo en .NET. A pesar de herramientas más modernas (Task, async/await), trabajar con Thread sigue teniendo sentido, especialmente si quieres sentirte como el dueño de los hilos "desde cero".
Esquema para crear un hilo
- Crear un objeto de la clase Thread, pasando el método que se ejecutará en el hilo.
- Arrancar el hilo con el método Start().
- (Opcional) Ver qué ocurre — ¡todo puede ejecutarse en paralelo!
2. Arrancar un hilo con Thread
Probemos sentir la magia del paralelismo. Añadamos a nuestra aplicación un pequeño clase que cuente hasta un número y muestre el progreso. Aprenderemos a ejecutar ese trabajo en un hilo aparte.
using System;
using System.Threading;
class Program
{
static void Main()
{
Console.WriteLine("¡El hilo principal ha arrancado!");
// Creamos el objeto Thread, indicando el método a ejecutar
Thread workerThread = new Thread(CountToTen);
// Arrancamos el nuevo hilo
workerThread.Start();
// El hilo principal también hace algo: escribe puntos...
for (int i = 0; i < 5; i++)
{
Console.Write(".");
Thread.Sleep(500); // Retardo para que se vea
}
Console.WriteLine("\n¡El hilo principal ha terminado!");
}
static void CountToTen()
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"[Hilo] Contando: {i}");
Thread.Sleep(400);
}
Console.WriteLine("[Hilo] ¡Listo!");
}
}
¿Qué pasa?
Verás en la consola que los puntos y "Contando: X" aparecen mezclados. ¡Ese es el primer signo de multihilo! El hilo principal escribe sus puntos, y el nuevo hilo cuenta hasta 10. No se estorban, como dos músicos en una banda: uno toca la batería y el otro el piano. Cada uno suena a su manera y juntos hacen música.
3. ¿Cómo pasar datos al hilo?
A veces el hilo debe saber no solo qué hacer, sino con qué trabajar. Si el método del hilo acepta parámetros, ¿cómo se los pasamos?
Opción 1: Usando una lambda (método anónimo)
int bounds = 7;
Thread t = new Thread(() => CountToNumber(bounds));
t.Start();
static void CountToNumber(int n)
{
for (int i = 1; i <= n; i++)
{
Console.WriteLine($"[Hilo] {i} / {n}");
Thread.Sleep(300);
}
}
Aquí envolvemos la llamada al método en una lambda para pasar parámetros. Es una práctica muy común, porque Thread espera un método sin parámetros (ThreadStart).
Opción 2: Usar ParameterizedThreadStart
Puedes usar el delegado especial ParameterizedThreadStart, que acepta un parámetro tipo object.
Thread t = new Thread(CountToNumberObject);
t.Start(12);
static void CountToNumberObject(object? n)
{
int max = (int)n!;
for (int i = 1; i <= max; i++)
{
Console.WriteLine($"[Hilo] {i} / {max}");
Thread.Sleep(200);
}
}
Sí, el tipo del parámetro es object, así que hace falta un cast. No es ideal, pero funciona. En C# moderno se prefiere la opción de la lambda.
4. Gestionar la vida del hilo
Veamos qué herramientas útiles ofrece la clase Thread.
| Propiedad / Método | Propósito |
|---|---|
|
Arranca el hilo (el método indicado al crear el hilo) |
|
Espera a que termine el hilo (bloquea el hilo que llama hasta que el otro termina) |
|
Indica si el hilo está corriendo ahora (true/false) |
|
Permite dar un nombre al hilo (útil para depuración) |
|
Obtener el objeto que representa el hilo actual |
|
Detiene el hilo actual durante ms milisegundos |
Ejemplo: Esperar a que termine un hilo
A veces el hilo principal necesita esperar a que termine un hilo auxiliar.
Thread t = new Thread(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"[Segundo hilo] {i}");
Thread.Sleep(300);
}
});
t.Start();
Console.WriteLine("[Hilo principal] Esperando a que termine el segundo hilo...");
t.Join(); // El hilo principal espera aquí
Console.WriteLine("[Hilo principal] ¡El segundo hilo ha terminado!");
Sin Join() el programa podría terminar incluso si el hilo sigue trabajando. Con Join() el hilo principal espera pacientemente a que todo acabe.
5. Matices útiles
Nombrar hilos: para no liarse
Para depuración puedes poner nombres a los hilos:
Thread t = new Thread(() =>
{
Console.WriteLine($"Esto se ejecuta en el hilo: {Thread.CurrentThread.Name}");
});
t.Name = "Hilo-Contador";
t.Start();
Esto ayuda cuando hay muchos hilos y cada uno hace su propia tarea.
Limitaciones y futuro real
Hay que decirlo: en aplicaciones modernas, gestionar hilos manualmente con Thread es raro. En la práctica se suelen usar herramientas más potentes e inteligentes (Task, async/await), que veremos más adelante. Pero entender cómo funcionan los hilos es importante para:
- Entender el "detrás de cámaras" de C# y .NET.
- Prepararse para entrevistas (a veces te pedirán explicar la diferencia entre Thread y Task).
- Diagnosticar y arreglar problemas complejos en aplicaciones grandes y heredadas.
Esquema final: ciclo de vida del hilo
stateDiagram-v2
[*] --> New: Thread creado
New --> Running: Start()
Running --> Stopped: Método terminado
Stopped --> [*]
Ahora sabes crear y arrancar hilos en C# por tu cuenta. Ya no eres solo un pasajero en el tren, eres el maquinista que controla varios vagones a la vez. Por delante: ciclo de vida del hilo, su gestión, sincronización y nuevos horizontes del paralelismo.
6. Errores típicos y trucos al trabajar con Thread
Error nº1: no arrancar el hilo.
A menudo se crea un objeto Thread pero se olvida llamar a Start(). Como resultado el hilo no comienza a trabajar, y puede ser difícil entender por qué.
Error nº2: modificar datos compartidos sin sincronización.
Si varios hilos trabajan con la misma variable sin protección, ¡espera problemas! Es como si dos cajeros dieran cambio desde la misma caja: pronto habrá confusión y errores.
Error nº3: usar métodos obsoletos y peligrosos.
No uses Thread.Suspend(), Thread.Resume() y similares — son peligrosos y obsoletos. Gestiona el ciclo de vida de los hilos con otras técnicas.
Error nº4: excepciones no capturadas dentro de hilos.
Si en un hilo ocurre una excepción no capturada, el hilo terminará y el hilo principal quizá ni se entere. Envuelve el código del hilo en un bloque try-catch para capturar errores y registrarlos.
Thread t = new Thread(() =>
{
try
{
// ... tu código
}
catch (Exception ex)
{
// Registramos el error
Console.WriteLine($"[Hilo] Error: {ex.Message}");
}
});
t.Start();
GO TO FULL VERSION