async, await and Tasks
A Task is work that may not have finished. async marks a method that can
suspend; await is where it suspends. The thread is not blocked while it waits -
it is returned to the pool and picks up other work, which is the whole point.
Console.WriteLine("before");
string name = await FetchNameAsync();
Console.WriteLine($"got {name}");
Console.WriteLine("after");
static async Task<string> FetchNameAsync()
{
await Task.Delay(50); // not Thread.Sleep: this does not hold a thread
return "Ada";
}
await at the top level works because the compiler wraps the file in an async
entry point. In a library the rule is: a method that awaits must be async, and
an async method should return Task or Task<T> - never void, except for an
event handler, because nothing can await it and nothing can catch what it throws.
Sequential await costs the sum; WhenAll costs the slowest
var clock = System.Diagnostics.Stopwatch.StartNew();
await LoadAsync("a", 60);
await LoadAsync("b", 60);
await LoadAsync("c", 60);
Console.WriteLine($"sequential took at least 180ms: {clock.ElapsedMilliseconds >= 180}");
clock.Restart();
// Start all three, THEN await. The tasks are already running by the time
// WhenAll is reached - which is the part people get wrong.
Task<string>[] running = [LoadAsync("a", 60), LoadAsync("b", 60), LoadAsync("c", 60)];
string[] results = await Task.WhenAll(running);
Console.WriteLine($"concurrent: {string.Join(",", results)} in about 60ms: "
+ $"{clock.ElapsedMilliseconds < 180}");
static async Task<string> LoadAsync(string name, int ms)
{
await Task.Delay(ms);
return name;
}
Cancellation is a parameter, not an exception you invent
Every async API in .NET takes a CancellationToken. Pass it down; do not
swallow the OperationCanceledException it produces.
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
try
{
await SlowAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("cancelled after 100ms, as asked");
}
// A timeout is the same mechanism with a clock attached.
using var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
string value = await FallbackAsync(timeout.Token);
Console.WriteLine(value);
static async Task SlowAsync(CancellationToken token)
{
await Task.Delay(TimeSpan.FromSeconds(5), token);
}
static async Task<string> FallbackAsync(CancellationToken token)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(5), token);
return "eventually";
}
catch (OperationCanceledException)
{
return "default, because it took too long";
}
}
Errors travel in the Task
An exception inside an async method is captured in the returned task and
rethrown at the await. That is what makes try/catch around an await work
the way it looks like it should.
try
{
await RiskyAsync(-1);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine($"caught at the await: {e.ParamName}");
}
// WhenAll reports the FIRST exception at the await, but the task holds them all.
Task all = Task.WhenAll(RiskyAsync(-1), RiskyAsync(-2));
try
{
await all;
}
catch (ArgumentOutOfRangeException)
{
Console.WriteLine($"WhenAll collected {all.Exception?.InnerExceptions.Count} failures");
}
static async Task<int> RiskyAsync(int n)
{
await Task.Yield();
return n < 0 ? throw new ArgumentOutOfRangeException(nameof(n)) : n * 2;
}
The two rules that prevent most async bugs
Never block on async code. .Result and .Wait() deadlock in any context
with a synchronisation context (classic ASP.NET, WinForms, WPF) and waste a
thread everywhere else. Await it, all the way up.
Do not async void. Nothing can await it, so nothing can observe the
failure: an exception from an async void method is thrown on the thread pool
and takes the process down.
// Right: async all the way, returning Task.
await GoodAsync();
// A fire-and-forget that at least observes its own failure.
_ = Task.Run(async () =>
{
try
{
await BackgroundAsync();
}
catch (Exception e)
{
Console.WriteLine($"background failed, and we know: {e.Message}");
}
});
await Task.Delay(100);
static async Task GoodAsync()
{
await Task.Delay(10);
Console.WriteLine("finished properly");
}
static async Task BackgroundAsync()
{
await Task.Delay(10);
throw new InvalidOperationException("something went wrong out of sight");
}
IAsyncEnumerable: results that arrive over time
await foreach (int value in CountToAsync(3))
{
Console.WriteLine($"got {value}");
}
Console.WriteLine("stream finished");
static async IAsyncEnumerable<int> CountToAsync(int n)
{
for (int i = 1; i <= n; i++)
{
await Task.Delay(10);
yield return i; // one value at a time, as it is ready
}
}
What to take away
awaitfrees the thread;Thread.Sleepand.Resulthold it.- An async method returns
Task/Task<T>-async voidonly for event handlers. - Start tasks, then
await Task.WhenAll- awaiting one at a time costs the sum. - Pass the
CancellationTokendown; a timeout is a token with a clock. - Exceptions are captured in the task and rethrown at the
await. await foreachover anIAsyncEnumerable<T>for results that arrive over time.