Sinds .NET Framework 4.5 is het mogelijk om asynchrone code te schrijven met async en await. Deze syntax maakt het eenvoudiger om niet-blokkerende code te schrijven. Asynchrone code is eigenlijk altijd aangeraden bij acties die niet processor gebonden zijn. Denk aan http verzoeken, I/O acties, netwerkverzoeken of database interacties. Ik merk in de praktijk dat async await door vrijwel alle teams die ik tegenkom gebruikt wordt. Tegelijk merk ik vaak dat ontwikkelaars vragen stellen over hoe dit nu eigenlijk werkt onder de motorkap, en welke valkuilen er zijn.
Onder de motorkap
Wanneer je een methode markeert met async, gebeurt het volgende:
- Compilertransformatie: De compiler herschrijft de methode naar een state machine. Deze machine houdt bij in welke fase van uitvoering de methode zich bevindt.
- await keyword: Wanneer je await gebruikt op een Task, wordt de uitvoering van de methode gepauzeerd totdat de Task voltooid is. De rest van de methode wordt opgeslagen als een vervolgactie (continuation).
- Context capturing: Standaard wordt de huidige synchronization context of task scheduler vastgelegd, zodat de voortzetting (continuation) op dezelfde context wordt uitgevoerd (bijvoorbeeld de UI-thread in een WPF-app).
Een eenvoudige async-methode:
Wordt door de compiler herschreven naar iets dat lijkt op:
- Een state machine met een MoveNext()-methode.
- Een TaskCompletionSource die het uiteindelijke resultaat bevat.
- Een await-punt dat een callback registreert voor wanneer de Task voltooid is.
Aandachtspunten bij het gebruik van async en await
Vergeten om await te gebruiken
De methode wordt dan gestart, maar niet afgewacht. Dit kan leiden tot onverwacht gedrag en race condities. In de praktijk geeft Visual Studio hier een waarschuwing die je beter in kunt stellen op een fout. In dit geval had er moeten staan “await SomeAsyncMethod();”.
Blocking met .Result() of .Wait()
Dit blokkeert de thread en kan leiden tot deadlocks, vooral in UI-contexten zoals WPF of WinForms. Zorg in dit geval dat de methode in ieder geval niet de UI thread blokkeert. In de praktijk kan dit vaak door gebruik te maken van asynchrone events of relaycommands:
Als je geen UI componenten aanroept na de await is het in veel gevallen voldoende om in de async aanroep de configure await uit te schakelen.
Het uitschakelen van de configure await, betekent dat de code op een willekeure nieuwe thread terug mag komen en dat de context van de originele thread niet behouden hoeft te worden.
Context capturing
Standaard zal de context van de aanroepende thread behouden worden als je met await een asynchrone methode aanroept. Dat is in bibliotheken en services vaak niet nodig, maar het kost wel performance. Om die reden kun je daar gebruik maken van ConfigureAwait(false).
Async void gebruiken buiten event handlers
Async void is eigenlijk altijd evil omdat het onmogelijk is om fatsoenlijk fouten af te vangen en het ook niet goed testbaar is. De enige uitzondering op deze regels zijn asynchrone event handlers.
Parallelisme verkeerd gebruikt
Hoewel in absolute zin misschien niet fout, is het in dit geval zonde om de taken sequentieel uit te voeren. Beter is het om beide taken te starten en dan te wachten op het resultaat:
Foutafhandeling
Foutafhandeling is vaak een ondergeschoven kindje in veel applicaties die ik tegenkom. Er zit meestal wel een soort “catch-all” constructie in. Vooral in asynchrone situaties kunnen fouten er toch wel eens doorheen slippen. Het is belangrijk om bewust na te denken op welke niveau’s in je software je fouten wilt afvangen en om heldere foutmeldingen te genereren. In een asynchrone methode is de stacktrace vaak al niet geweldig, dus dan helpt het enorm als er een duidelijke strategie in de software zit om fouten af te handelen.
Conclusie
Async en await maken asynchrone programmering toegankelijker, maar vereisen zorgvuldige toepassing om fouten en performanceproblemen te voorkomen. Door bewust te zijn van de onderliggende werking en valkuilen kun je robuuste en efficiënte asynchrone code schrijven.



