# Task Coordination

Course: [C# Advanced](https://www.devtutorials.ai/learn/advanced-csharp) · Section: Asynchronous Programming
Source: https://www.devtutorials.ai/learn/advanced-csharp/asynchronous-programming/task-coordination

> Run many tasks together: Task.WhenAll, Task.WhenAny, results, errors from many tasks, timeouts, throttling, retries and common async patterns.

## Introduction to Task Coordination

Imagine a restaurant kitchen during dinner time.

One order includes a soup, a curry, and a dessert. Three cooks prepare them **at the same time**. The waiter does not serve the soup alone. The waiter waits until **all three** dishes are ready, then serves them together.

Sometimes, the kitchen works differently. The manager calls three suppliers for more rice and buys from **whichever answers first**.

This is **task coordination**: starting several tasks, then deciding how to wait for them. You can wait for all of them, or just the first one.

**In simple words:** task coordination means running many async operations together and collecting their results in a controlled way.

In the last lesson, you started two tasks and awaited them one by one. That works for two. Real programs often start ten or a thousand: one per file, one per customer, one per web page. This lesson shows the tools that handle a whole group at once.

**Easy Way to Remember:** `WhenAll` means "**wait for everyone**". `WhenAny` means "**first one wins**".

## What You Will Learn

1. How to run many tasks at the same time.
2. `Task.WhenAll` and `Task.WhenAny`.
3. How to collect and process results from many tasks.
4. How to handle exceptions when several tasks fail.
5. How to put a time limit on a task.
6. How to combine independent tasks and run work in parallel.
7. How to write async code that scales, and the common async patterns.
8. Why coordination makes programs faster.

**About the Run buttons in this lesson.** The examples run in your browser on .NET in WebAssembly. Real web calls cannot run there, so `Task.Delay` stands in for the slow part. It waits just like a network call would.

## Start With a Working Example

Press **Run**.

```csharp runnable title="Three dishes cook at once"
using System;
using System.Threading.Tasks;

Console.WriteLine("Order received");

Task<string> soup = CookAsync("Soup", 300);
Task<string> curry = CookAsync("Curry", 500);
Task<string> dessert = CookAsync("Dessert", 200);

string[] dishes = await Task.WhenAll(soup, curry, dessert);

Console.WriteLine($"Serving: {string.Join(", ", dishes)}");

static async Task<string> CookAsync(string dish, int ms)
{
    Console.WriteLine($"Start {dish}");
    await Task.Delay(ms);
    Console.WriteLine($"{dish} ready");
    return dish;
}
```

All three dishes start right away. They finish in order of speed, but `WhenAll` gives back the results **in the order you passed the tasks in**, not the order they finished.

### Try This

Change the curry's time from `500` to `100`. The curry now finishes first, but the "Serving" line stays in the same order.

## Running Multiple Tasks

To run tasks at the same time, **start them all first**, then wait.

Calling an async method starts it. So a loop that calls the method and stores each task in a list starts every operation almost at once.

```csharp runnable title="Start every task in a loop"
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

string[] files = { "a.txt", "b.txt", "c.txt", "d.txt" };

var tasks = new List<Task<int>>();
foreach (string file in files)
{
    tasks.Add(GetSizeAsync(file)); // starts now, does not wait
}
Console.WriteLine($"Started {tasks.Count} tasks");

int[] sizes = await Task.WhenAll(tasks);
for (int i = 0; i < files.Length; i++)
    Console.WriteLine($"{files[i]}: {sizes[i]} KB");

static async Task<int> GetSizeAsync(string file)
{
    await Task.Delay(100 * file.Length);
    return file.Length * 10;
}
```

A shorter way to start one task per item is LINQ's `Select`:

```text
Task<int>[] tasks = files.Select(file => GetSizeAsync(file)).ToArray();
```

If you put `await` **inside** the loop, the tasks run one after another, which is what you are trying to avoid. Start the tasks in the loop, and `await` once **after** the loop.

## Task.WhenAll

**`Task.WhenAll`** takes a group of tasks and returns **one** task that finishes when **every** task in the group has finished.

| You pass in | `await Task.WhenAll(...)` gives you |
|---|---|
| `Task` objects (no results) | Nothing, it just waits |
| `Task<T>` objects | A `T[]` array, in the same order as the tasks |

The total time is the time of the **slowest** task, not the sum of all of them.

```csharp runnable title="WhenAll with and without results"
using System;
using System.Threading.Tasks;

// Tasks with no result: WhenAll just waits.
await Task.WhenAll(
    SendAsync("Asha", 200),
    SendAsync("Ravi", 100),
    SendAsync("Meena", 300));
Console.WriteLine("All emails sent");

// Tasks with a result: WhenAll gives back an array.
int[] scores = await Task.WhenAll(ScoreAsync(70), ScoreAsync(85), ScoreAsync(92));
Console.WriteLine($"Scores: {string.Join(", ", scores)}");

static async Task SendAsync(string name, int ms)
{
    await Task.Delay(ms);
    Console.WriteLine($"Email sent to {name}");
}

static async Task<int> ScoreAsync(int score)
{
    await Task.Delay(100);
    return score;
}
```

## Task.WhenAny

**`Task.WhenAny`** returns as soon as **one** task in the group finishes. The result is **the task that finished first**, so you `await` it a second time to get its value.

```csharp runnable title="The fastest server wins"
using System;
using System.Threading.Tasks;

Task<string> serverA = AskServerAsync("Server A", 400);
Task<string> serverB = AskServerAsync("Server B", 150);
Task<string> serverC = AskServerAsync("Server C", 300);

Task<string> winner = await Task.WhenAny(serverA, serverB, serverC);
string answer = await winner; // unwrap the winning task's result

Console.WriteLine($"First answer: {answer}");
Console.WriteLine($"Was it Server B? {winner == serverB}");

static async Task<string> AskServerAsync(string name, int ms)
{
    await Task.Delay(ms);
    return $"data from {name}";
}
```

The other two servers keep running in the background. `WhenAny` does not stop them. Lesson 3 shows how to cancel the others.

| | `Task.WhenAll` | `Task.WhenAny` |
|---|---|---|
| Finishes when | **Every** task has finished | **One** task has finished |
| `await` gives you | All results, as an array | The finished **task** (await it again for its value) |
| Typical use | Load everything a page needs | Fastest server, timeouts, "first reply wins" |

## Waiting for Multiple Operations

Sometimes you want to react to **each** task as soon as it finishes, instead of waiting for the whole group.

The pattern is a loop with `WhenAny`: wait for any task, handle it, remove it from the list, repeat until the list is empty.

```csharp runnable title="Handle each download the moment it finishes"
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

var pending = new List<Task<string>>
{
    DownloadAsync("video.mp4", 500),
    DownloadAsync("photo.jpg", 100),
    DownloadAsync("song.mp3", 300),
};

int done = 0;
while (pending.Count > 0)
{
    Task<string> finished = await Task.WhenAny(pending);
    pending.Remove(finished);
    done++;
    Console.WriteLine($"[{done}/3] {await finished} finished");
}
Console.WriteLine("All downloads complete");

static async Task<string> DownloadAsync(string file, int ms)
{
    await Task.Delay(ms);
    return file;
}
```

This is how a progress bar can move forward as each file arrives, instead of jumping from 0% to 100% at the end.

## Processing Results from Multiple Tasks

`WhenAll` gives you an array of results. From there, it is ordinary C#: loop over it, add it up, or use LINQ.

```csharp runnable title="Add up prices from three shops"
using System;
using System.Linq;
using System.Threading.Tasks;

string[] shops = { "Shop A", "Shop B", "Shop C" };

decimal[] prices = await Task.WhenAll(shops.Select(GetPriceAsync));

for (int i = 0; i < shops.Length; i++)
    Console.WriteLine($"{shops[i]}: {prices[i]}");

Console.WriteLine($"Cheapest: {prices.Min()}");
Console.WriteLine($"Average:  {prices.Average():0.00}");

int cheapestIndex = Array.IndexOf(prices, prices.Min());
Console.WriteLine($"Buy from {shops[cheapestIndex]}");

static async Task<decimal> GetPriceAsync(string shop)
{
    await Task.Delay(100);
    return shop switch
    {
        "Shop A" => 499m,
        "Shop B" => 459m,
        _ => 520m,
    };
}
```

Because `WhenAll` keeps the original order, `prices[i]` always belongs to `shops[i]`.

## Handling Exceptions in Multiple Tasks

When one task in a `WhenAll` group fails, `await Task.WhenAll(...)` throws. Two things to know:

- `WhenAll` still **waits for every task** to finish, even after one has failed.
- `await` throws only the **first** exception. The full list is on the `WhenAll` task's `.Exception.InnerExceptions`.

```csharp runnable title="Two of four uploads fail"
using System;
using System.Threading.Tasks;

Task[] uploads =
{
    UploadAsync("a.jpg", 100, fail: false),
    UploadAsync("b.jpg", 200, fail: true),
    UploadAsync("c.jpg", 300, fail: false),
    UploadAsync("d.jpg", 400, fail: true),
};

Task all = Task.WhenAll(uploads);
try
{
    await all;
}
catch (Exception first)
{
    Console.WriteLine($"await threw: {first.Message}");
    Console.WriteLine($"Total failures: {all.Exception!.InnerExceptions.Count}");
    foreach (Exception ex in all.Exception.InnerExceptions)
        Console.WriteLine($"  - {ex.Message}");
}

int succeeded = Array.FindAll(uploads, t => t.IsCompletedSuccessfully).Length;
Console.WriteLine($"Succeeded: {succeeded} of {uploads.Length}");

static async Task UploadAsync(string file, int ms, bool fail)
{
    await Task.Delay(ms);
    if (fail) throw new InvalidOperationException($"{file} upload failed");
    Console.WriteLine($"{file} uploaded");
}
```

Keep the `WhenAll` task in a variable (`all` above) before awaiting it. That is the only way to reach the full list of errors.

If you want **every** task to report its own result, success or failure, catch inside each task instead, and return a status:

```csharp runnable title="Each task reports success or failure"
using System;
using System.Threading.Tasks;

string[] results = await Task.WhenAll(
    TryUploadAsync("a.jpg", fail: false),
    TryUploadAsync("b.jpg", fail: true),
    TryUploadAsync("c.jpg", fail: false));

foreach (string r in results)
    Console.WriteLine(r);

static async Task<string> TryUploadAsync(string file, bool fail)
{
    try
    {
        await Task.Delay(100);
        if (fail) throw new InvalidOperationException("server error");
        return $"{file}: OK";
    }
    catch (Exception ex)
    {
        return $"{file}: FAILED ({ex.Message})";
    }
}
```

## Task Timeouts

A slow server should not make your program wait forever. A **timeout** says: "if this is not done in 2 seconds, give up".

There are two common ways.

**1. `WaitAsync(timeout)`** (.NET 6 and later). It throws a `TimeoutException` if the task is not done in time.

```csharp runnable title="WaitAsync throws when time runs out"
using System;
using System.Threading.Tasks;

foreach (int ms in new[] { 100, 900 })
{
    try
    {
        string data = await LoadAsync(ms).WaitAsync(TimeSpan.FromMilliseconds(500));
        Console.WriteLine($"{ms} ms request: got {data}");
    }
    catch (TimeoutException)
    {
        Console.WriteLine($"{ms} ms request: timed out after 500 ms");
    }
}

static async Task<string> LoadAsync(int ms)
{
    await Task.Delay(ms);
    return "data";
}
```

**2. `WhenAny` with `Task.Delay`.** Race the real task against a timer. Whichever finishes first wins.

```csharp runnable title="Race the task against a timer"
using System;
using System.Threading.Tasks;

Task<string> request = LoadAsync(800);
Task timer = Task.Delay(300);

Task first = await Task.WhenAny(request, timer);

if (first == request)
    Console.WriteLine($"Got: {await request}");
else
    Console.WriteLine("Too slow, showing cached data instead");

static async Task<string> LoadAsync(int ms)
{
    await Task.Delay(ms);
    return "fresh data";
}
```

A timeout stops **you from waiting**. It does not stop the slow task itself, which keeps running in the background. To really stop the work, use cancellation (lesson 3).

## Combining Independent Tasks

Two tasks are **independent** when neither needs the other's result. Independent tasks should always run together.

A product page is a good example. The product details, the reviews, and the stock level all come from different places, and none of them needs the others.

```csharp runnable title="Load a product page: three independent calls"
using System;
using System.Threading.Tasks;

Task<string> detailsTask = GetDetailsAsync(42);
Task<double> ratingTask = GetRatingAsync(42);
Task<int> stockTask = GetStockAsync(42);

await Task.WhenAll(detailsTask, ratingTask, stockTask);

// All three are finished, so these awaits return at once.
string details = await detailsTask;
double rating = await ratingTask;
int stock = await stockTask;

Console.WriteLine(details);
Console.WriteLine($"Rating: {rating} / 5");
Console.WriteLine(stock > 0 ? $"In stock: {stock}" : "Out of stock");

static async Task<string> GetDetailsAsync(int id) { await Task.Delay(300); return $"Product {id}: Desk lamp"; }
static async Task<double> GetRatingAsync(int id) { await Task.Delay(200); return 4.5; }
static async Task<int> GetStockAsync(int id) { await Task.Delay(100); return 12; }
```

The three tasks return **different types**, so they cannot share one array. Keep each task in its own variable, use `WhenAll` to wait for all, then `await` each one to read its value.

When tasks **depend** on each other, keep the order, and only run the independent parts together:

```text
User user = await GetUserAsync(id);                 // needed first
Task<Order[]> orders = GetOrdersAsync(user.Id);     // both need user,
Task<Address> address = GetAddressAsync(user.Id);   // but not each other
await Task.WhenAll(orders, address);
```

## Parallel Async Operations

Starting a thousand tasks at once can overload a server, or hit a rate limit such as "at most 3 requests at a time".

**`SemaphoreSlim`** works like a set of entry tickets. Each task takes a ticket before it starts and gives it back when it finishes. When all tickets are taken, the next task waits.

```csharp runnable title="At most 2 downloads at a time"
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

var gate = new SemaphoreSlim(2); // 2 tickets
int running = 0, maxRunning = 0;

string[] files = { "f1", "f2", "f3", "f4", "f5", "f6" };
await Task.WhenAll(files.Select(DownloadAsync));

Console.WriteLine($"All {files.Length} files done");
Console.WriteLine($"Most running at the same time: {maxRunning}");

async Task DownloadAsync(string file)
{
    await gate.WaitAsync();           // take a ticket (wait if none left)
    try
    {
        running++;
        maxRunning = Math.Max(maxRunning, running);
        await Task.Delay(100);
        running--;
    }
    finally
    {
        gate.Release();               // give the ticket back
    }
}
```

.NET 6 and later also has **`Parallel.ForEachAsync`**, which does the same thing in one call. It uses real background threads, so run it on your own computer:

```text
await Parallel.ForEachAsync(
    files,
    new ParallelOptions { MaxDegreeOfParallelism = 3 },
    async (file, token) =>
    {
        await DownloadAsync(file, token);
    });
```

## Writing Scalable Async Code

**Scalable** code keeps working well when the load grows from 10 users to 10,000.

| Rule | Why it matters |
|---|---|
| Start independent work together | Total time is the slowest task, not the sum |
| Limit how many run at once | Protects servers, databases, and rate limits |
| Never block with `.Result` or `.Wait()` | A blocked thread cannot serve anyone else |
| Use real async I/O methods | `ReadAsync`, `GetAsync`, `SaveChangesAsync` free the thread while waiting |
| Keep CPU work out of hot async paths | Heavy calculation holds a thread just like blocking does |
| Always add a timeout to network calls | One slow server should not freeze every request |

A small example of the difference. Ten requests of 100 ms each:

```csharp runnable title="Ten requests: in a row, or all together"
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

var sw = Stopwatch.StartNew();
for (int i = 0; i < 10; i++)
    await RequestAsync();
long inARow = sw.ElapsedMilliseconds;

sw.Restart();
await Task.WhenAll(Enumerable.Range(0, 10).Select(_ => RequestAsync()));
long together = sw.ElapsedMilliseconds;

Console.WriteLine($"In a row is slower: {inARow > together}");
Console.WriteLine($"In a row took at least 1 second: {inARow >= 1000}");
Console.WriteLine($"Together took under half a second: {together < 500}");

static Task RequestAsync() => Task.Delay(100);
```

## Common Async Patterns

These patterns appear again and again in real code.

| Pattern | What it does | Tool |
|---|---|---|
| **Fan-out / fan-in** | Start many tasks, then collect all results | `Select` + `WhenAll` |
| **First wins** | Use whichever source answers first | `WhenAny` |
| **Timeout** | Give up after a time limit | `WaitAsync` or `WhenAny` + `Task.Delay` |
| **Process as completed** | Handle each result as it arrives | `WhenAny` in a loop |
| **Throttling** | Limit how many run at once | `SemaphoreSlim` or `Parallel.ForEachAsync` |
| **Retry** | Try again after a failure, with a pause | loop + `try`/`catch` + `Task.Delay` |

The **retry** pattern is worth seeing in full, because network calls often fail once and then succeed:

```csharp runnable title="Retry up to 3 times"
using System;
using System.Threading.Tasks;

int attempts = 0;

string result = await RetryAsync(async () =>
{
    attempts++;
    await Task.Delay(50);
    if (attempts < 3) throw new InvalidOperationException("server busy");
    return "order placed";
}, maxTries: 3);

Console.WriteLine($"Result: {result} (after {attempts} attempts)");

static async Task<T> RetryAsync<T>(Func<Task<T>> action, int maxTries)
{
    for (int attempt = 1; ; attempt++)
    {
        try
        {
            return await action();
        }
        catch (Exception ex) when (attempt < maxTries)
        {
            Console.WriteLine($"Attempt {attempt} failed: {ex.Message}. Retrying...");
            await Task.Delay(100 * attempt); // wait a little longer each time
        }
    }
}
```

The `when (attempt < maxTries)` filter lets the last failure escape to the caller, so a request that never succeeds still reports its error.

## Performance Benefits of Task Coordination

Coordination does not make any single operation faster. It removes the **waiting in between**.

| Approach | 3 calls of 200, 300 and 500 ms | Why |
|---|---|---|
| Await one by one | about 1000 ms | The waits add up |
| `WhenAll` | about 500 ms | The waits overlap, only the slowest counts |
| `WhenAny` | about 200 ms | Only the fastest counts |

On a web server, the benefit is even bigger. While a request waits for the database, its thread is free to start other requests. The same server can handle many more users without any extra hardware.

Coordination pays off for **waiting** work (network, disk, database, timers). For heavy **calculation**, the limit is the number of CPU cores, and the tools are `Parallel.For` and PLINQ, covered in the Multithreading chapter.

## Common mistakes

| Mistake | Wrong | Correct | Why |
|---|---|---|---|
| Awaiting inside the loop | `foreach (...) await LoadAsync(f);` | `await Task.WhenAll(tasks);` | The tasks run one at a time |
| Using the WhenAny result as the value | `await Task.WhenAny(a, b)` | `await await Task.WhenAny(a, b)` | WhenAny returns the finished task |
| Expecting finishing order from WhenAll | `results[0]` is the fastest | Use WhenAny in a loop | WhenAll keeps the order you passed in |
| Losing all but the first error | `catch` after `await WhenAll` | Read `all.Exception.InnerExceptions` | await rethrows only the first error |
| Thinking a timeout cancels the work | `await t.WaitAsync(time);` | Also pass a CancellationToken | A timeout only stops the waiting |
| Starting too many at once | 10,000 tasks in one WhenAll | Limit with `SemaphoreSlim` | Servers reject floods of requests |
| Blocking on a group | `Task.WaitAll(tasks);` | `await Task.WhenAll(tasks);` | Blocks the thread; fails in the browser |

## What to Remember

- Start independent tasks first, then wait for them together.
- `Task.WhenAll` waits for **every** task and returns all results in the order you passed them in.
- `Task.WhenAny` returns the **first** task to finish; `await` it again for its value.
- `WhenAny` in a loop lets you handle each result as soon as it arrives.
- When tasks in `WhenAll` fail, `await` throws the first error; `.Exception.InnerExceptions` holds them all.
- Use `WaitAsync` or `WhenAny` with `Task.Delay` for timeouts. A timeout stops the waiting, not the task.
- Limit how many tasks run at once with `SemaphoreSlim` or `Parallel.ForEachAsync`.
- Common patterns: fan-out/fan-in, first wins, timeout, process as completed, throttling, retry.
- Coordination saves time by overlapping waits: the total is the slowest task, not the sum.

Next: Cancellation
