CodeGym /Cursos /C# SELF /Gestión de múltiples tareas

Gestión de múltiples tareas

C# SELF
Nivel 60, Lección 2
Disponible

1. Task.WhenAll: esperar a todas a la vez

En la vida real rara vez haces solo una cosa. Te levantas de la cama y a la vez miras el reloj, te duchas y cantas, te vistes y escuchas música mientras preparas el café. O si trabajas en un ordenador, subes varios archivos al mismo tiempo, envías un lote de peticiones de red, calculas un montón de datos en distintos frentes. Todas esas acciones pueden (¡y deben!) ejecutarse en paralelo para no hacer perder tiempo al usuario. No queremos esperar a que cada tarea termine una por una. ¿Cómo organizar esa "orquesta"? ¿Cómo saber cuándo ha terminado TODO? ¿O, al revés, saber quién terminó primero?

Task.WhenAll y Task.WhenAny son las herramientas que necesitas para estos escenarios.

Método WhenAll

Task.WhenAll es un método estático de la clase Task que acepta una colección de tareas y devuelve una nueva tarea que se completará solo cuando hayan finalizado todas las tareas pasadas. Es como esperar en un examen hasta que todos los compañeros entreguen sus exámenes antes de salir del aula.

Firma del método

Task Task.WhenAll(params Task[] tasks)
Task<TResult[]> Task.WhenAll<TResult>(params Task<TResult>[] tasks)

Hay versiones para tareas que devuelven resultados (Task<TResult>) y para las "vacías" (Task).

Ejemplo sencillo: esperar la descarga de todos los archivos

Vamos a intentar descargar tres archivos de forma asíncrona. Para simplificar — simulamos la descarga con un retraso (Task.Delay). Los ejemplos son reales y el código funciona.

using System;
using System.Threading.Tasks;

class Program
{
    // Simulamos la descarga de un archivo con un retraso, devolvemos el nombre
    static async Task<string> DownloadFileAsync(string fileName)
    {
        Console.WriteLine($"► Comenzando descarga: {fileName}");
        await Task.Delay(2000); // Esperamos 2 segundos
        Console.WriteLine($"✓ Descarga completada: {fileName}");
        return fileName;
    }

    static async Task Main()
    {
        var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };

        // Iniciamos todas las descargas al mismo tiempo
        var downloadTasks = new Task<string>[files.Length];
        for (int i = 0; i < files.Length; i++)
        {
            downloadTasks[i] = DownloadFileAsync(files[i]);
        }

        // Esperamos a que todas las descargas terminen
        string[] results = await Task.WhenAll(downloadTasks);

        Console.WriteLine($"¡Todas las descargas han terminado! Lista: {string.Join(", ", results)}");
    }
}

¿Qué pasa aquí?

  • No esperamos cada tarea de forma secuencial. Las tres se inician en paralelo (asíncronamente).
  • Task.WhenAll(downloadTasks) devuelve una tarea que se completará solo cuando todas las tareas hayan finalizado.
  • Después de eso podemos trabajar con los resultados de todas las tareas — usarlos, mostrarlos en pantalla, enviarlos a otro sitio.

Análogo

Es parecido a encargar a tres mensajeros que entreguen tres paquetes y avisar al jefe solo cuando todos hayan llegado con éxito a los clientes.

Diagrama de funcionamiento de Task.WhenAll

sequenceDiagram
    participant Program
    participant Tarea1
    participant Tarea2
    participant Tarea3

    Program->>Tarea1: Inicio
    Program->>Tarea2: Inicio
    Program->>Tarea3: Inicio
    Tarea1-->>Program: Finalizada (puede ser antes que las demás)
    Tarea2-->>Program: Finalizada
    Tarea3-->>Program: Finalizada
    Program->>Program: Task.WhenAll completado, todos los resultados disponibles

¿Qué pasa si una de las tareas termina con error?

Task.WhenAll no se interrumpe cuando alguna tarea falla. Espera a que todas terminen. Si al menos una lanzó una excepción, la tarea resultante quedará en estado Faulted, y contendrá la colección de todas las excepciones ocurridas.

Ejemplo

static async Task<string> MayThrowAsync(string fileName)
{
    await Task.Delay(500);
    if (fileName == "fileB.txt")
        throw new Exception("Error de descarga fileB.txt");
    return fileName;
}

static async Task Main()
{
    var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };
    var downloadTasks = files.Select(MayThrowAsync).ToArray();
    try
    {
        string[] results = await Task.WhenAll(downloadTasks);
        Console.WriteLine($"Todo bien: {string.Join(", ", results)}");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Al menos una tarea terminó con error: {ex.Message}");
    }
}

En caso de excepción, para ver todos los errores internos usa AggregateException.InnerExceptions.
Documentación oficial: Task.WhenAll docs

2. Task.WhenAny: esperar a la primera tarea que termine

Task.WhenAny también es un método estático, pero se completará tan pronto como cualquiera de las tareas pasadas pase al estado "Completed" (da igual si con error o con éxito). Devuelve una referencia a la primera tarea que haya terminado.

Firma

Task<Task> Task.WhenAny(params Task[] tasks)
Task<Task<TResult>> Task.WhenAny<TResult>(params Task<TResult>[] tasks)

Análogo

Quien haga el pastel primero — tiene mérito, y a los demás se les puede pedir que paren. A veces basta saber qué servidor respondió más rápido y usar su resultado.

Ejemplo: ¿quién es más rápido?

using System;
using System.Threading.Tasks;

class Program
{
    // Simulamos una petición con distinta velocidad
    static async Task<string> RequestAsync(string name, int delay)
    {
        await Task.Delay(delay);
        return $"{name} terminó en {delay} ms";
    }

    static async Task Main()
    {
        var taskA = RequestAsync("A", 1000);
        var taskB = RequestAsync("B", 700);    // El más rápido
        var taskC = RequestAsync("C", 1500);

        // Esperamos la primera tarea que termine
        Task<string> finished = await Task.WhenAny(taskA, taskB, taskC);

        Console.WriteLine($"El primero en terminar: {finished.Result}");
    }
}

Con WhenAny sabemos qué tarea llegó primero a la meta. Después de eso puedes cancelar las demás tareas, por ejemplo usando CancellationToken (tema de próximas lecciones).

¿Por qué WhenAny devuelve la tarea y no el resultado?

Porque el método no sabe de antemano qué tipo de resultado devolverá la tarea que termine primero: no puede saber cuál terminará antes. Por eso devuelve la propia tarea — luego tú extraes el resultado con await o con la propiedad Result de esa tarea.

WhenAll vs WhenAny: ilustración comparativa

Método Cuándo actúa Qué devuelve Escenario típico
Task.WhenAll
Cuando todas han terminado Tarea (Task/Task<T[]>) Esperamos todas las respuestas o todos los archivos, agregamos resultados
Task.WhenAny
Cuando alguna de las tareas termina La tarea que terminó primero Usamos el primer resultado, cancelamos las demás

3. Combinando WhenAll y WhenAny — escenarios prácticos

A veces necesitas primero esperar a cualquier tarea y luego a todas. O al revés.

Ejemplo: "Ping-pong" entre dos servidores

Imagina que envías la petición a dos servidores clon por si uno va lento. Pero usas el resultado del primero.

static async Task Main()
{
    var fastServer = RequestAsync("Servidor rápido", 400);   // 400 ms
    var slowServer = RequestAsync("Servidor lento", 2000); // 2000 ms

    var completed = await Task.WhenAny(fastServer, slowServer);
    Console.WriteLine(await completed);

    // *Opcional*: aquí puedes cancelar las tareas que aún no hayan terminado con CancellationToken
}

Ejemplo: empezar el procesamiento solo tras cargar todos los datos

En tu aplicación tienes tres fuentes de datos. Solo cuando las tres hayan cargado vale la pena pasar al procesamiento:

static async Task Main()
{
    var task1 = DownloadFileAsync("a.txt");
    var task2 = DownloadFileAsync("b.txt");
    var task3 = DownloadFileAsync("c.txt");

    var all = await Task.WhenAll(task1, task2, task3);

    ProcessFiles(all[0], all[1], all[2]);
}

4. Errores típicos al trabajar con Task.WhenAll y Task.WhenAny

Error nº1: esperar que WhenAll termine con la primera tarea.
Los principiantes piensan que Task.WhenAll se completará cuando termine la primera tarea. En realidad espera a que todas las tareas terminen.

Error nº2: ignorar excepciones en WhenAll.
Si alguna tarea lanza una excepción, la tarea resultante pasará a estado Faulted. Si no manejas AggregateException.InnerExceptions puedes perder errores importantes.

Error nº3: acceder a Result sin comprobar en WhenAny.
Si la primera tarea que terminó falló, llamar a Result lanzará la excepción. Comprueba Task.IsFaulted antes de acceder.

Error nº4: arrancar tareas secuencialmente en lugar de en paralelo.
Por ejemplo, esperar cada tarea con await dentro de un bucle en vez de usar Task.WhenAll reduce mucho el rendimiento.

2
Tarea
C# SELF, nivel 60, lección 2
Bloqueada
Uso de Task.WhenAll para ejecutar varias tareas
Uso de Task.WhenAll para ejecutar varias tareas
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION