Concurrencia en C#: hilos, asincronía y gorutinas en la práctica

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Concurrencia en C#: cómo evitar que el programa se aburra

\n\n

Imagina que eres un chef. Tienes tres fogones, un horno y una batidora. Si haces todo de forma secuencial —primero cortar cebolla, luego freír, luego batir la crema— la comida tardará una eternidad. Es mucho más eficiente encender el horno mientras cortas verduras y batir la crema mientras se guisa la carne. Esto es la concurrencia: la capacidad de ejecutar varias tareas "simultáneamente", alternando entre ellas para que los recursos no estén inactivos.

\n\n

En C# hay tres herramientas principales para esto: los hilos clásicos (Thread), las tareas modernas (Task) con async/await y el concepto de gorutinas implementado mediante iteradores. En este artículo analizaremos cada enfoque, compararemos su rendimiento y mostraremos cuándo usar cada uno. Si programas en C# y quieres que tu código vuele en lugar de arrastrarse, has llegado al lugar indicado.

\n\n

Hilos (Thread): artillería pesada

\n\n

Un hilo es la forma más básica de organizar la concurrencia. Cada hilo es un "proceso ligero" independiente dentro de tu programa, con su propia pila y contexto. Crear un hilo en C# es muy sencillo:

\n\n
using System;\nusing System.Threading;\n\nclass Program\n{\n    static void Main()\n    {\n        Thread thread = new Thread(DoWork);\n        thread.Start();\n        Console.WriteLine("El hilo principal está funcionando");\n        thread.Join(); // esperamos a que termine\n    }\n\n    static void DoWork()\n    {\n        for (int i = 0; i < 5; i++)\n        {\n            Console.WriteLine($"Hilo: {i}");\n            Thread.Sleep(200);\n        }\n    }\n}
\n\n

Aquí hemos creado un hilo separado que cuenta hasta cinco, mientras el hilo principal muestra un mensaje. Sin Join, el programa podría terminar antes de llegar a cinco.

\n\n

Desventajas de los hilos

\n
    \n
  • Pesados. Cada hilo reserva aproximadamente 1 MB de memoria para la pila. Crear 1000 hilos significa ocupar 1 GB solo por eso.
  • \n
  • Complejidad de sincronización. Para que dos hilos no corrompan datos compartidos, se necesitan locks, mutex y semáforos. Un error y obtienes un deadlock o una condición de carrera.
  • \n
  • Cambio de contexto. El sistema operativo pierde tiempo alternando entre hilos. Si hay más hilos que núcleos de CPU, esto se convierte en un cuello de botella.
  • \n
\n\n

Los hilos son útiles cuando necesitas paralelismo real en un procesador multinúcleo para cálculos pesados. Pero para entrada/salida (trabajo con archivos, red, base de datos) son excesivos.

\n\n

Task y async/await: el estándar moderno

\n\n

A partir de .NET 4.5, Microsoft propuso una alternativa elegante: la Task Parallel Library (TPL) y las palabras clave async/await. No son hilos en sentido estricto, sino promesas: le dices al compilador "ejecuta este trabajo y, cuando termine, devuélveme el resultado". Mientras el trabajo se ejecuta, el hilo no se bloquea, sino que vuelve al pool.

\n\n
using System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\n\nclass Program\n{\n    static async Task Main()\n    {\n        Console.WriteLine("Comenzando la descarga...");\n        string data = await DownloadDataAsync();\n        Console.WriteLine($"Descargado: {data.Length} caracteres");\n    }\n\n    static async Task<string> DownloadDataAsync()\n    {\n        using HttpClient client = new HttpClient();\n        string result = await client.GetStringAsync("https://example.com");\n        return result;\n    }\n}
\n\n

Observa: await no bloquea el hilo. Mientras los datos se descargan por la red, el hilo vuelve al pool y puede atender otras solicitudes. Esto es ideal para servidores web y aplicaciones GUI: la interfaz no se congela.

\n\n

¿Cómo funciona internamente?

\n

Cuando el compilador ve async, convierte el método en una máquina de estados. En cada await, el método "recuerda" su estado

Blogs

Book Recommendations