SkillByAIOpen interactive version →

Lesson 17 / 25

Concurrency with Tasks and Cancellation

Run tasks concurrently, cancel them, and stream results asynchronously.

Coordinating many operations

Task.WhenAll awaits several tasks started together and returns all results, so three independent 1-second calls take about 1 second rather than 3. Task.WhenAny completes when the first task finishes, useful for timeouts or racing replicas. Cancellation is cooperative: pass a CancellationToken down through every async method, and libraries such as HttpClient and EF Core stop work when it is cancelled, throwing OperationCanceledException. Create tokens with a CancellationTokenSource, for example new CancellationTokenSource(TimeSpan.FromSeconds(5)) for a timeout. IAsyncEnumerable<T> with await foreach streams results that arrive asynchronously, such as pages of an API. Parallel.ForEachAsync processes many items with a limit on concurrency. ValueTask<T> can avoid an allocation when results are often available synchronously, but must not be awaited twice.

WhenAll, a timeout and limited parallelism

The token flows into every call so a slow dependency cannot hang the request forever.

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var ct = cts.Token;

// three independent calls run concurrently
Task<decimal> priceTask = GetPriceAsync("sku-1", ct);
Task<int> stockTask = GetStockAsync("sku-1", ct);
Task<double> ratingTask = GetRatingAsync("sku-1", ct);
await Task.WhenAll(priceTask, stockTask, ratingTask);
Console.WriteLine($"{priceTask.Result} / {stockTask.Result} / {ratingTask.Result}");  // safe: already completed

// process 1,000 images, at most 8 at a time
var files = Directory.GetFiles("uploads", "*.jpg");
await Parallel.ForEachAsync(files, new ParallelOptions { MaxDegreeOfParallelism = 8, CancellationToken = ct },
    async (file, token) => await ResizeAsync(file, token));

// streaming results
await foreach (var page in GetPagesAsync(ct))
    Console.WriteLine(page.Count);

Ordering at a food court

Awaiting one stall at a time is queueing at each counter in turn. Task.WhenAll is placing orders at three stalls, then collecting all three trays when the buzzers go off.

Quick check: How should a long-running async operation be stopped when the user cancels a request?

  • Call Thread.Abort
  • Pass a CancellationToken through the call chain and let operations observe it
  • Use .Result to block it
  • Throw an exception from another thread
Answer

Pass a CancellationToken through the call chain and let operations observe it — Cancellation in .NET is cooperative via CancellationToken.