CodeGym /Courses /C# SELF /Managing multiple tasks

Managing multiple tasks

C# SELF
Level 60, Lesson 2
Available

1. Task.WhenAll: wait for all at once

In real life it's rare that you only do one thing. You get out of bed while checking the time, take a shower and sing, get dressed and listen to music while making coffee. Or when working on a computer you might download several files at once, send a bunch of network requests, calculate lots of data in different directions. All these actions can (and should!) run in parallel so you don't waste the user's time. We don't want to wait for each task to finish one by one! How to organize such an "orchestra"? How to know when EVERYTHING is done? Or, conversely, find out who finished first?

Task.WhenAll and Task.WhenAny are the tools you need for these scenarios.

WhenAll method

Task.WhenAll is a static method of the Task class that accepts a collection of tasks and returns a new task that completes only when all provided tasks have finished. It's like waiting at an exam until all your classmates hand in their tickets before leaving the room.

Method signature

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

There are versions for tasks that return results (Task<TResult>) and for "void" ones (Task).

Simple example: waiting for all files to download

Let's try to download three files asynchronously. For simplicity — we'll simulate downloads with a delay (Task.Delay). Examples are realistic; code is runnable.

using System;
using System.Threading.Tasks;

class Program
{
    // Simulate file download with a delay, return the name
    static async Task<string> DownloadFileAsync(string fileName)
    {
        Console.WriteLine($"► Starting download: {fileName}");
        await Task.Delay(2000); // Wait 2 seconds
        Console.WriteLine($"✓ Download finished: {fileName}");
        return fileName;
    }

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

        // Start all downloads at the same time
        var downloadTasks = new Task<string>[files.Length];
        for (int i = 0; i < files.Length; i++)
        {
            downloadTasks[i] = DownloadFileAsync(files[i]);
        }

        // Wait for all downloads to complete
        string[] results = await Task.WhenAll(downloadTasks);

        Console.WriteLine($"All downloads completed! List: {string.Join(", ", results)}");
    }
}

What's happening here?

  • We don't wait for each task one by one. All three are started in parallel (asynchronously).
  • Task.WhenAll(downloadTasks) returns a task that will complete only when all tasks complete.
  • After that we can work with results of all tasks — use them, print them to the console, send them further.

Analogy

It's like assigning three couriers to deliver three packages and telling your boss only when all of them have successfully reached the clients.

How Task.WhenAll works

sequenceDiagram
    participant Program
    participant Task1
    participant Task2
    participant Task3

    Program->>Task1: Start
    Program->>Task2: Start
    Program->>Task3: Start
    Task1-->>Program: Completed (may finish before others)
    Task2-->>Program: Completed
    Task3-->>Program: Completed
    Program->>Program: Task.WhenAll completed, all results available

What if one of the tasks fails?

Task.WhenAll doesn't stop when one task fails. It waits for all tasks to finish. If at least one threw an exception — the resulting task will be in Faulted state, and it will contain a collection of all exceptions that occurred.

Example

static async Task<string> MayThrowAsync(string fileName)
{
    await Task.Delay(500);
    if (fileName == "fileB.txt")
        throw new Exception("Error downloading 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($"All good: {string.Join(", ", results)}");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"At least one task finished with an error: {ex.Message}");
    }
}

In case of an exception, to inspect all inner errors use AggregateException.InnerExceptions.
Official docs: Task.WhenAll docs

2. Task.WhenAny: wait for the first completed task

Task.WhenAny is also a static method, but it completes as soon as any of the provided tasks transitions to a "Completed" state (regardless of success or failure). It returns a reference to the first task that completed.

Signature

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

Analogy

Whoever bakes the pie first — kudos to them, you can ask the others to stop. Sometimes it's enough to know which of several servers answered fastest and use that result.

Example: who's faster?

using System;
using System.Threading.Tasks;

class Program
{
    // Simulate request with different speed
    static async Task<string> RequestAsync(string name, int delay)
    {
        await Task.Delay(delay);
        return $"{name} finished in {delay} ms";
    }

    static async Task Main()
    {
        var taskA = RequestAsync("A", 1000);
        var taskB = RequestAsync("B", 700);    // Fastest
        var taskC = RequestAsync("C", 1500);

        // Wait for the first completed task
        Task<string> finished = await Task.WhenAny(taskA, taskB, taskC);

        Console.WriteLine($"First finished: {finished.Result}");
    }
}

With WhenAny we find out which task got to the finish line first. After that you can cancel the other tasks, for example using CancellationToken (topic of later lectures).

Why does WhenAny return a task and not a result?

Because the method doesn't know what result type the tasks have: it can't know in advance which task will finish first. So it returns the task itself — then you need to extract the result yourself: via await or the Result property on that task.

WhenAll vs WhenAny: comparative illustration

Method When it triggers What it returns Typical scenario
Task.WhenAll
When all have finished A task (Task/Task<T[]>) Wait for all responses or files, aggregate results
Task.WhenAny
When any one of the tasks has finished The task of the very first completed task Use the first result, cancel the rest

3. Combining WhenAll and WhenAny — real-life scenarios

Sometimes you need to wait for any task first, and then for all. Or the other way around.

Example: "ping-pong" between two servers

Imagine you send requests to two server clones in case one is slow. But you use whichever result comes first.

static async Task Main()
{
    var fastServer = RequestAsync("Fast server", 400);   // 400 ms
    var slowServer = RequestAsync("Slow server", 2000); // 2000 ms

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

    // *Optionally*: here you can cancel remaining incomplete tasks via CancellationToken
}

Example: start processing only after all data is loaded

Your app has three data sources. Only when all three are loaded should you move to processing:

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. Common mistakes when working with Task.WhenAll and Task.WhenAny

Mistake #1: expecting WhenAll to finish when the first task completes.
Beginners think that Task.WhenAll will complete as soon as the first task finishes. In reality it waits for all tasks to complete.

Mistake #2: ignoring exceptions in WhenAll.
If one of the tasks throws an exception, the resulting task will be in Faulted state. If you don't handle AggregateException.InnerExceptions, you might miss important errors.

Mistake #3: accessing Result without checks on WhenAny.
If the first completed task failed, calling Result will throw. Check Task.IsFaulted before accessing it.

Mistake #4: starting tasks sequentially instead of in parallel.
For example, awaiting each task inside a loop instead of using Task.WhenAll drastically reduces performance.

2
Task
C# SELF, level 60, lesson 2
Locked
Using Task.WhenAll to Run Multiple Tasks
Using Task.WhenAll to Run Multiple Tasks
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION