Beetje Coderen

Les 4

Async en await in C#

Asynchroon werken met Task.

⏱ 45–60 minuten leestijd + oefenen

🎯 Na deze les kun je

  • async-methodes schrijven en aanroepen
  • uitleggen waarom webservers async gebruiken
  • deadlocks en .Result vermijden

Een webserver wacht veel. Op de database, op een andere API, op een bestand. In die tijd doet de processor niets. Met async en await kan je programma tijdens het wachten ander werk doen, zoals een volgend verzoek afhandelen. Heel ASP.NET Core is daarop gebouwd, dus je ziet async in bijna elke les hierna. Als je in de HTML-CSS-cursus fetch met await hebt gebruikt, herken je het idee; anders leer je het hier vanaf het begin.

Het probleem: wachten

using var http = new HttpClient();
string html = http.GetStringAsync("https://example.com").Result;   // niet doen

Deze code werkt, maar de thread (een uitvoeringslijn van je programma) zit stil te wachten tot het antwoord binnen is. In een console-app maakt dat weinig uit. Op een webserver wel: daar heeft de server een beperkt aantal threads. Als elk verzoek er één vasthoudt terwijl het op de database wacht, zijn ze bij druk allemaal bezet. Nieuwe bezoekers moeten dan wachten, terwijl de processor eigenlijk niks doet.

async en await

using var http = new HttpClient();
string html = await http.GetStringAsync("https://example.com");
Console.WriteLine(html.Length);
  • GetStringAsync begint het verzoek en geeft meteen een Task<string> terug: een belofte dat er later een string komt. (In JavaScript heet dat een Promise.)
  • await zegt: "ga hier verder als de taak klaar is, en geef me dan de waarde". Tijdens het wachten is de thread vrij voor ander werk.
  • Daarna gaat de methode verder alsof het gewone, regel-voor-regel code is.

Een eigen async-methode

static async Task<int> HaalLengteAsync(HttpClient http, string url)
{
    string html = await http.GetStringAsync(url);
    return html.Length;
}
Onderdeel Betekenis
async in deze methode mag await voorkomen
Task<int> geeft (later) een int
Task geeft niets terug, maar je kunt wel wachten tot hij klaar is (de async-versie van void)
...Async afspraak: async-methoden eindigen op Async

Je returnt gewoon een int; de compiler pakt hem in een Task<int> in. Wie de methode aanroept, gebruikt weer await. Zo werkt async zich door je hele code omhoog. Dat is de bedoeling: async all the way.

Wat gebeurt er precies?

Voorspel de volgorde van de uitvoer:

Console.WriteLine("A");
var taak = WachtAsync();
Console.WriteLine("C");
await taak;

static async Task WachtAsync()
{
    Console.WriteLine("B1");
    await Task.Delay(100);
    Console.WriteLine("B2");
}
A
B1
C
B2

Een async-methode loopt synchroon tot de eerste await op iets wat nog niet klaar is. Daar geeft hij een onvoltooide taak terug aan de aanroeper, die verdergaat ("C"). Na 100 ms gaat WachtAsync verder ("B2"). Pas bij await taak wacht het hoofdprogramma tot alles klaar is.

Meerdere dingen tegelijk

Omdat een taak al loopt zodra je de methode aanroept, kun je meerdere taken starten en daarna op allemaal tegelijk wachten:

string[] urls = ["https://example.com", "https://www.rijksoverheid.nl", "https://dotnet.microsoft.com"];

// na elkaar
foreach (var url in urls)
{
    var html = await http.GetStringAsync(url);
    Console.WriteLine($"{url}: {html.Length} tekens");
}

// tegelijk
var taken = urls.Select(url => HaalLengteAsync(http, url));
int[] lengtes = await Task.WhenAll(taken);

string[] urls = [ ... ] is een collection expression: een korte schrijfwijze voor een array.

Task.WhenAll wacht tot alle taken klaar zijn en geeft de resultaten in dezelfde volgorde als de taken, ongeacht welke het eerst binnenkwam. Bij een proefrun duurde "na elkaar" ruim een seconde, "tegelijk" ongeveer zo lang als het traagste verzoek. Exacte tijden verschillen per verbinding.

Tegelijk is niet altijd beter. Duizend verzoeken tegelijk naar dezelfde server kan die server overbelasten of je laten blokkeren. Voor grote aantallen beperk je hoeveel er tegelijk lopen, bijvoorbeeld met Parallel.ForEachAsync en MaxDegreeOfParallelism.

Fouten

Een fout in een async-methode wordt bij de await gegooid, dus try/catch werkt zoals je verwacht:

try
{
    await http.GetStringAsync("https://example.com/bestaat-niet");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Mislukt: {(int?)ex.StatusCode} {ex.Message}");
}
Mislukt: 404 Response status code does not indicate success: 404 (Not Found).

Anders dan fetch in JavaScript gooit GetStringAsync wél een fout bij een 404 of 500.

.Result en .Wait(): niet doen

Je komt in oude code en op internet .Result, .Wait() of .GetAwaiter().GetResult() tegen. Die laten de thread blokkerend wachten op een taak.

  • Je verliest het hele voordeel van async: de thread zit weer stil.
  • In sommige omgevingen (oude ASP.NET, Windows-desktopapps) veroorzaakt het een deadlock: de taak wil verder op de thread die op hem wacht, en die thread wacht op de taak. Het programma hangt dan voorgoed.
  • Fouten komen verpakt in een AggregateException, wat lastiger te lezen is.

ASP.NET Core heeft dat deadlockprobleem niet meer, maar de eerste twee punten gelden nog steeds. Regel: als je in een async-methode zit, gebruik await. Moet je echt in synchrone code op een taak wachten (zelden nodig), weet dan dat je iets bijzonders doet.

Nog een valkuil: async void. Gebruik dat alleen voor event handlers in desktopapps. Een async void-methode kun je niet awaiten, en een fout daarin laat het hele programma crashen. Gebruik altijd async Task.

CancellationToken

Als een bezoeker de pagina sluit terwijl de server nog bezig is, heeft verder werken geen zin. Veel async-methoden accepteren daarom een CancellationToken:

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
try
{
    var html = await http.GetStringAsync("https://example.com", cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Te traag, afgebroken.");
}

In ASP.NET Core krijg je een token cadeau dat afgaat als de bezoeker afhaakt. Geef het door aan database- en HTTP-aanroepen. Dat zie je in les 06.

Typische fouten

Symptoom Oorzaak
The 'await' operator can only be used within an async method async vergeten in de methodedeclaratie
Cannot implicitly convert type 'Task<string>' to 'string' await vergeten
Waarschuwing "this call is not awaited" Taak gestart maar niet afgewacht; fouten raken zoek
App hangt .Result of .Wait() in een omgeving met deadlockgevaar
Crash zonder nette fout async void

Oefeningen

Oefening 1. Maak van deze methode een async-versie:

static string LeesBestand(string pad) => File.ReadAllText(pad);
Modelantwoord oefening 1
static async Task<string> LeesBestandAsync(string pad) => await File.ReadAllTextAsync(pad);

Zonder async/await mag ook: static Task<string> LeesBestandAsync(string pad) => File.ReadAllTextAsync(pad);

Oefening 2. Drie taken duren 1, 2 en 3 seconden. Hoe lang duurt het ongeveer met drie keer await na elkaar, en hoe lang met Task.WhenAll?

Modelantwoord oefening 2

Na elkaar ongeveer 6 seconden, met Task.WhenAll ongeveer 3 (de langste).

Oefening 3. Wat is er mis?

public async Task<IActionResult> Index()
{
    var producten = _service.HaalProductenAsync().Result;
    return View(producten);
}
Modelantwoord oefening 3

.Result blokkeert de thread. Schrijf var producten = await _service.HaalProductenAsync();.

Lab: meerdere URL's tegelijk ophalen

  1. Maak een console-app die een lijst URL's uit een tekstbestand leest.
  2. Haal ze eerst na elkaar en daarna tegelijk op met Task.WhenAll, en meet beide met Stopwatch.
  3. Vang fouten per URL af, zodat één kapotte URL de rest niet tegenhoudt; toon per URL de statuscode of de fout.
  4. Breek elk verzoek af na 5 seconden met een CancellationToken.
  5. beetje check aspnet-les4

Samenvatting

  • async markeert een methode, await wacht op een Task zonder de thread te blokkeren.
  • Een methode geeft Task of Task<T> terug en heet ...Async; async werkt zich door tot bovenaan.
  • Task.WhenAll voert taken tegelijk uit en geeft resultaten in vaste volgorde.
  • Vermijd .Result, .Wait() en async void; geef CancellationTokens door.

Volgende les: hoe het web werkt. Wat stuurt een browser eigenlijk naar je server?

Lab

Meerdere URL's tegelijk ophalen

💻 Dit lab doe je op je eigen computer met Docker. Start de lab-container met (geeft de opdracht een eigen startcommando, gebruik dan dat):

$ docker run -it --rm registry.wizg.it/webhosting/beetje-labs:aspnet-les4

Volg daarna de stappen van de opdracht hierboven. Nog geen Docker? Zie les 14 van Een beetje Linux & Docker. Voor dit lab is er geen live lab in de browser.

Klaar? Controleer je werk met:

$ beetje check aspnet-les4

📝 Quiz: async en await

5 vragen · 70% nodig om te slagen

1. Waarom gebruiken webservers async?
2. Wat geeft een async-methode met return 5 terug?
3. Wat doet Task.WhenAll?
4. Waarom vermijd je .Result?
5. Wat is het probleem met async void?