async/await, Task and CancellationToken
Asynchrony in .NET is a powerful tool, but it has its traps: deadlocks with
.Result, missing cancellation, sneaky async void. This lesson gathers the
ProfessNet house rules for async code in C#.
Task and async/await
An async method returns Task (no result) or Task<T> (with a result).
await does not block the thread — it releases it for the duration of the I/O
operation.
public async Task<ScanResult> RunScanAsync(string forest)
{
var hosts = await _repository.GetHostsAsync(forest);
var result = await _scanner.ScanAsync(hosts);
return result;
}
Never .Result or .Wait()
Blocking on asynchronous code causes deadlocks (especially in an ASP.NET context) and wastes threads.
// bad — risk of deadlock, blocks the thread
var result = RunScanAsync(forest).Result;
// good — await all the way up
var result = await RunScanAsync(forest);
ProfessNet standard: the "async all the way" rule. If a method calls something asynchronous, it is itself
asyncand propagates that to the caller..Resultand.Wait()are forbidden in application code.
CancellationToken propagates everywhere
Every public async method accepts a CancellationToken and passes it on. This
makes long operations cancellable (request shutdown, timeout).
public async Task<ScanResult> RunScanAsync(
string forest,
CancellationToken cancellationToken = default)
{
var hosts = await _repository.GetHostsAsync(forest, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
return await _scanner.ScanAsync(hosts, cancellationToken);
}
In ASP.NET Core you get the token from the action parameter:
[HttpPost("scan")]
public async Task<IActionResult> Scan(ScanRequest req, CancellationToken ct)
{
var result = await _service.RunScanAsync(req.Forest, ct);
return Accepted(result);
}
ProfessNet standard:
CancellationTokenis the last parameter of an async method, defaulting todefault. We pass it to every subsequent async call.
async void only in event handlers
async void cannot be awaited and its exceptions cannot be caught — it is a
source of silent failures. Everywhere except event handlers we use Task.
// bad — the exception disappears, you can't await it
public async void Process() { await DoWork(); }
// good
public async Task ProcessAsync() { await DoWork(); }
Parallelism: Task.WhenAll
We run independent operations concurrently.
var tasks = forests.Select(f => ScanForestAsync(f, ct));
ScanResult[] results = await Task.WhenAll(tasks);
| Construct | When |
|---|---|
await | a single async operation |
Task.WhenAll | many operations concurrently |
CancellationToken | every public async method |
ConfigureAwait(false) | library code (without a UI/request context) |
Tip: in libraries (not in ASP.NET) use
ConfigureAwait(false)so you don't return to the original synchronization context — this is a small performance gain and protection against deadlocks.
Async all the way, zero .Result, a cancellation token in every method. Stick
to this and async code in .NET will be efficient, cancellable and free of
deadlocks.