A3.A Tareas a la vez¶
Para quién es esto
Es material avanzado: da por sabidas las unidades 1 a 5, en especial excepciones y clases. Aquí las esperas son de decenas de milisegundos para que las salidas sean siempre las mismas.
Un programa que hace una cosa tras otra pierde mucho tiempo esperando: una respuesta de la red, un archivo, un temporizador. La concurrencia consiste en aprovechar esa espera para hacer otras cosas, y la asincronía en no bloquear el programa mientras se espera.
El modelo de C#: Task, async y await¶
C# tiene un modelo muy extendido y fácil de leer, que han copiado otros lenguajes:
| Pieza | Qué es |
|---|---|
Task / Task<T> |
Una operación en curso, que terminará (o fallará) más tarde; con T, además producirá un resultado |
async |
Marca un método que puede esperar sin bloquear; devuelve un Task |
await |
«Espera a que esta tarea termine y sigue»; mientras espera, el hilo queda libre para otras cosas |
Task.Delay(ms) |
Una espera de verdad asíncrona (lo contrario de Thread.Sleep, que bloquea el hilo) |
Task.Run(...) |
Ejecuta código de cálculo en un hilo del pool |
Task.WhenAll(...) / WhenAny(...) |
Esperar a todas / a la primera |
Un await no crea hilos: reanuda el método cuando la espera termina. Los hilos solo aparecen con Task.Run o cuando el trabajo lo exige. Por eso el mismo programa puede atender miles de esperas con muy pocos hilos.
Un ejemplo: tres tareas a la vez¶
Tres «descargas» con una pausa cada una. Primero una tras otra y luego a la vez:
using System.Diagnostics;
async Task Descargar(string nombre, int ms)
{
await Task.Delay(ms); // espera SIN bloquear el hilo
Console.WriteLine($"termina {nombre}");
}
// Una tras otra
var reloj = Stopwatch.StartNew();
await Descargar("uno", 200);
await Descargar("dos", 50);
await Descargar("tres", 100);
bool secuencial = reloj.ElapsedMilliseconds >= 340;
Console.WriteLine("--- ahora a la vez ---");
// Las tres a la vez: se lanzan todas y luego se espera a todas
reloj.Restart();
await Task.WhenAll(Descargar("uno", 200), Descargar("dos", 50), Descargar("tres", 100));
bool simultaneo = reloj.ElapsedMilliseconds < 340;
Console.WriteLine($"una tras otra tarda 350 ms o más: {secuencial}");
Console.WriteLine($"a la vez tarda menos de 340 ms: {simultaneo}");
Salida:
termina uno
termina dos
termina tres
--- ahora a la vez ---
termina dos
termina tres
termina uno
una tras otra tarda 350 ms o más: True
a la vez tarda menos de 340 ms: True
- Con
awaitcada tarea antes de lanzar la siguiente, el tiempo se suma (200 + 50 + 100 = 350 ms). - Con
Task.WhenAll, se lanzan las tres (llamar a un métodoasyncya empieza la tarea) y después se espera a todas: el tiempo es el de la más lenta (200 ms). Se ve en el orden en que terminan:dos,tres,uno, que no es el orden en que se lanzaron.
Qué usar en cada caso¶
| Situación | Herramienta |
|---|---|
| Esperar algo externo (red, disco, temporizador) | async/await con Task.Delay, HttpClient, File.ReadAllTextAsync... |
| Calcular mucho (CPU) sin bloquear la interfaz | Task.Run(() => ...) |
| Repartir un bucle de cálculo entre núcleos | Parallel.For / Parallel.ForEach |
| Un hilo de verdad, con vida propia | new Thread(...) (raro: casi siempre basta Task) |
int principal = Environment.CurrentManagedThreadId;
int dentroDeTask = await Task.Run(() => Environment.CurrentManagedThreadId);
Console.WriteLine($"Task.Run usa otro hilo: {principal != dentroDeTask}");
// Parallel.For reparte un bucle de CÁLCULO entre los núcleos
long suma = 0;
Parallel.For(1, 101, i => Interlocked.Add(ref suma, i));
Console.WriteLine($"suma en paralelo: {suma}");
Salida:
Task.Run usa otro hilo: True
suma en paralelo: 5050
No bloquees una tarea
Dentro de un método async, no uses Thread.Sleep, .Result ni .Wait(): bloquean el hilo y pueden dejar el programa colgado (sobre todo en aplicaciones con ventana). Usa await.
Errores frecuentes¶
| Error | Cómo evitarlo |
|---|---|
Thread.Sleep dentro de código async |
await Task.Delay(...) |
Hacer await de cada tarea nada más lanzarla (queda secuencial) |
Lanzarlas todas y esperar después con Task.WhenAll |
Llamar a un método async y no esperar su Task |
await siempre (o guardar el Task): si no, los errores se pierden |
async void |
Usar async Task; async void solo en manejadores de eventos |
Usar Task.Run para esperar una respuesta de red |
La espera ya es asíncrona; Task.Run es para cálculo |
Para practicar¶
Los ejercicios A3.1 y A3.2 usan estas ideas. Para ver la misma idea escrita en los otros lenguajes: Pasar de uno a otro.