C# / .NET/02core12 min

async/await, Task i CancellationToken

Asynchroniczność w .NET to potężne narzędzie, ale ma swoje pułapki: deadlocki z .Result, brak anulowania, fałszywe async void. Ta lekcja zbiera house rules ProfessNet dla kodu async w C#.

Task i async/await

Metoda async zwraca Task (bez wyniku) lub Task<T> (z wynikiem). await nie blokuje wątku — zwalnia go na czas operacji I/O.

public async Task<ScanResult> RunScanAsync(string forest)
{
    var hosts = await _repository.GetHostsAsync(forest);
    var result = await _scanner.ScanAsync(hosts);
    return result;
}

Nigdy .Result ani .Wait()

Blokowanie na asynchronicznym kodzie powoduje deadlocki (zwłaszcza w kontekście ASP.NET) i marnuje wątki.

// źle — ryzyko deadlocku, blokuje wątek
var result = RunScanAsync(forest).Result;

// dobrze — await aż do góry
var result = await RunScanAsync(forest);

Standard ProfessNet: zasada „async all the way". Jeśli metoda woła coś asynchronicznego, sama jest async i propaguje to do wywołującego. .Result i .Wait() są zakazane w kodzie aplikacyjnym.

CancellationToken propaguje się wszędzie

Każda publiczna metoda async przyjmuje CancellationToken i przekazuje go dalej. Dzięki temu długie operacje da się anulować (zamknięcie żądania, 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);
}

W ASP.NET Core token dostajesz z parametru akcji:

[HttpPost("scan")]
public async Task<IActionResult> Scan(ScanRequest req, CancellationToken ct)
{
    var result = await _service.RunScanAsync(req.Forest, ct);
    return Accepted(result);
}

Standard ProfessNet: CancellationToken to ostatni parametr metody async, domyślnie default. Przekazujemy go do każdego kolejnego wywołania async.

async void tylko w event handlerach

async void nie da się await-ować ani złapać wyjątku — to źródło cichych awarii. Wszędzie poza handlerami zdarzeń używamy Task.

// źle — wyjątek zniknie, nie da się poczekać
public async void Process() { await DoWork(); }

// dobrze
public async Task ProcessAsync() { await DoWork(); }

Równoległość: Task.WhenAll

Niezależne operacje uruchamiamy współbieżnie.

var tasks = forests.Select(f => ScanForestAsync(f, ct));
ScanResult[] results = await Task.WhenAll(tasks);
KonstrukcjaKiedy
awaitpojedyncza operacja async
Task.WhenAllwiele operacji współbieżnie
CancellationTokenkażda publiczna metoda async
ConfigureAwait(false)kod bibliotek (bez kontekstu UI/żądania)

Wskazówka: w bibliotekach (nie w ASP.NET) używaj ConfigureAwait(false), żeby nie wracać na oryginalny kontekst synchronizacji — to drobny zysk wydajności i ochrona przed deadlockami.


Async all the way, zero .Result, token anulowania w każdej metodzie. Trzymaj się tego, a kod async w .NET będzie wydajny, anulowalny i wolny od deadlocków.