Reading — step 1 of 5
Learn
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
OperationCanceledExceptionand swallowing it — surfaces as a bug; let it propagate. - Mixing
.Resultwithawait— 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…