Valkuilen in async/await

Het is inmiddels alweer behoorlijk wat jaren geleden dat Microsoft async/await introduceerde in het .NET Framework. Het gebruik van async/await heeft het mogelijk gemaakt om met heel weinig code asynchroon te programmeren. Helaas betekent dit niet dat het eenvoudig is om asynchroon te programmeren, en ik heb mijzelf meermalen in de voet geschoten doordat ik te kort door te bocht wilde gaan. Bij deze wat valkuilen waar ik tegenaan ben gelopen, maar eerst even de twee belangrijkste redenen om asynchroon te programmeren.

Waarom async/await een goede zaak is

Er zijn eigenlijk twee belangrijke redenen om async/await toe te passen. In webapplicaties zal de webserver (afhankelijk van de gekozen technologie) maar een beperkt aantal threads tegelijk in een workerprocess afhandelen. Dit betekent dat bij een groot aantal gelijktijdige verzoeken de webserver processen in de wacht gaat zetten. Door gebruik te maken van async/await, wordt de calling thread van het workerprocess vrijwel direct vrijgegeven, terwijl een background thread de bulk van het werk uitvoert. In client applicaties (Windows en apps) zijn asynchrone processen vooral van belang omdat de UI-thread wordt vrijgegeven en de applicatie dus goed blijft reageren op de gebruiker, terwijl processen op de achtergrond worden afgehandeld. Elke langlopende taak (database, I/O of zware berekening) leent zich om in een asynchrone taak af te handelen.

Valkuil 1: Deadlocks

In het voorbeeld hieronder wordt het resultaat van een taak uitgevraagd. Stel dat dit een asynchrone methode is, die zelf ook gebruik maakt van async/await, dan creëer je een deadlock. Dat komt omdat het aanroepen van Result, de UI-thread in de wacht zet totdat het resultaat van de methode terugkomt. Als de methode zelf een await commando gebruikt, zal deze een thread opstarten en na afloop het werk willen teruggeven aan de UI-thread. Dat kan echter niet omdat deze UI-thread aan het wachten is op het resultaat van de functie die hij nu zelf zou moeten uitvoeren.

Ofschoon dit fenomeen vooral voor zal komen in een Windows applicatie of app, is het niet uit te sluiten dat deadlocks voorkomen in een webapplicatie wanneer je zelf de asynchrone taken beheert en niet gebruik maakt van de asynchrone controllers van ASP.NET.

Oplossing

Zorg dat de methode nadat deze gereed is, wordt opgevolgd door een nieuwe asynchrone taak die het resterende werk uitvoert. In de praktijk is het aan te raden om een class op te zetten die alle uitkomsten van een taak correct afhandelt, want er komt nogal wat bij kijken om alle foutafhandeling en cancellation scenario’s in te bouwen.

Valkuil 2: UI werk op een niet-UI-thread

In het voorbeeld hierboven werd het resultaat van een taak gebruikt in op een label de content te veranderen. Dit gaat fout in Windowsapplicaties en apps wanneer een andere thread dan de UI-thread deze code uitvoert.

Oplossing

Stuur het werk dat naar de UI-thread moet via de Dispatcher naar de UI-thread. Deze is toegankelijk vanaf een window/usercontrol of via het Application object. In de praktijk schijf ik er altijd een proxy met interface voor, zodat ik de code ook kan unittesten.

Valkuil 3: incorrecte/geen foutafhandeling

Binnen een async/await context worden fouten netjes doorgegeven aan de aanroepende thread. Wanneer je aan het einde van de call-chain aankomt, is het heel gemakkelijk om de foutafhandeling te vergeten.

Bovenstaande code zal zonder problemen draaien, maar indien er in de ExecuteAsync een fout plaatsvindt zal deze niet zichtbaar worden in de aanroepende thread. Gebruik om dezelfde reden geen async void (tenzij je zeker weet wat je doet), maar retourneer altijd een taak zodat eventuele fouten afgehandeld kunnen worden. Wanneer je de code aanpast naar ExecuteAsync().Wait() zal er wel een fout optreden, echter dan krijg je weer deadlock problemen.

Oplossing

Schrijf, zoals in een eerder voorbeeld, een juiste continuatie taak waarin eventuele fouten worden afgehandeld.

Schrijf wat er moet gebeuren wanneer er een fout optreedt (bij voorkeur wat charmanter dan MessageBox.Show).

Valkuil 4: await alleen als het nodig is

In het bovenstaande voorbeeld wordt de ReadFileAsync methode aangeroepen. De aanroepende methode zal bij het await keyword een nieuwe thread opstarten om het asynchrone werk op te pakken. De ReadFileAsync methode zal nogmaals een thread opstarten bij het await keyword. Deze laatste is onnodig omdat de waarde van ReadToEndAsync in deze methode niet gebruikt wordt. In dit geval is het beter om geen async/await te gebruiken, maar rechtstreeks een Task te retourneren.

Oplossing

Geef een taak terug zodat er slechts een thread opgestart wordt bij de eerste await. Let wel op dat je in dit geval de boundaries van de code goed in acht neemt. De using statements moeten hier binnen de taak liggen en niet erbuiten anders zijn deze objecten al opgeruimd terwijl de taak nog draait.

Valkuil 5: start niet onnodig threads op

Los van de zaken die in bovenstaande code niet juist zijn, zoals het niet afhandelen van fouten en onhandig aanmaken van de HttpClient, wordt er voor elke parallelle actie nu een extra thread gemaakt voor het uitvoeren van de post. Dat betekent een thread vanuit de task parallel library en een vanuit async/await.

Oplossing

Bij een groot aantal url’s is de beste oplossing om wel de task parallel library te gebruiken, maar hierbinnen de post synchroon te houden. Bij een klein aantal url’s kun je beter een lijst van taken retourneren waar je de code op laat wachten.

Auteur: Menno Jongerius, Bergler Competence Center © 2020

Deel deze pagina via:
berglerValkuilen in async/await