Skip to content
Tasks in Parallel and Cancellation
step 1/5

Reading — step 1 of 5

Learn

~2 min readModern C#

You've seen async/await for one async operation. Real apps run many in parallel — Task.WhenAll, Task.WhenAny, and CancellationToken are how you orchestrate them.

Task.WhenAll — wait for all

async Task<User[]> FetchUsers(int[] ids)
{
    Task<User>[] tasks = ids.Select(id => FetchUser(id)).ToArray();
    return await Task.WhenAll(tasks);     // returns User[] — all results
}

Launches all in parallel, awaits all. Total time = max(individual times), not sum. If any throws, the resulting task fails (after all complete).

Task.WhenAny — first to complete

async Task<string> FastestEndpoint(string[] urls)
{
    Task<string>[] requests = urls.Select(GetAsync).ToArray();
    Task<string> winner = await Task.WhenAny(requests);
    return await winner;
}

Returns when the FIRST task completes. Useful for racing servers, falling back to faster mirrors.

CancellationToken — cooperative cancellation

Long-running async operations should accept a CancellationToken and check it:

async Task<int> SumAsync(IEnumerable<int> source, CancellationToken ct)
{
    int total = 0;
    foreach (var n in source)
    {
        ct.ThrowIfCancellationRequested();   // throws OperationCanceledException
        total += n;
        await Task.Delay(10, ct);             // also responds to cancellation
    }
    return total;
}

Callers create a CancellationTokenSource:

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
    int result = await SumAsync(numbers, cts.Token);
    Console.WriteLine(result);
}
catch (OperationCanceledException)
{
    Console.WriteLine("timed out");
}

Cancellation is COOPERATIVE — the work has to check the token. There's no kill-9 for tasks.

Combined — race with timeout

async Task<T?> WithTimeout<T>(Task<T> task, TimeSpan timeout)
{
    var cts = new CancellationTokenSource();
    Task winner = await Task.WhenAny(task, Task.Delay(timeout, cts.Token));
    if (winner == task)
    {
        cts.Cancel();   // stop the delay early
        return await task;
    }
    return default;     // timed out
}

Common utility — wait for a task with a timeout, returning default on miss.

Avoiding common deadlocks

Never block on .Result or .Wait() in code that runs on a captured sync context (UI thread, ASP.NET request thread):

// BUG — deadlocks in UI/ASP.NET classic:
var data = FetchAsync().Result;

// Right:
var data = await FetchAsync();

In modern ASP.NET Core (no sync context) it's less risky, but it's still bad practice.

ConfigureAwait(false)

In library code that doesn't need the original sync context, add ConfigureAwait(false) to every await to avoid deadlocks AND improve perf:

public async Task<string> LoadAsync()
{
    var data = await DownloadAsync().ConfigureAwait(false);
    return Process(data);
}

In application code (UI/ASP.NET classic), omit it so await resumes on the original context.

Common mistakes

  • Awaiting in a loop sequentially when independent items could run in parallel — silently slow.
  • Not cancelling work the user navigated away from — wastes CPU and may trigger errors later.
  • Catching OperationCanceledException and swallowing it — surfaces as a bug; let it propagate.
  • Mixing .Result with await — recipe for deadlocks.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…