Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
Het TAP-model (Task asynchroon programmeren) biedt een abstractielaag ten opzichte van typische asynchrone codering. In dit model schrijft u code als een reeks instructies, net zoals gebruikelijk. Het verschil is dat u uw op taken gebaseerde code kunt lezen terwijl de compiler elke instructie verwerkt en voordat de volgende instructie wordt verwerkt. Om dit model te bereiken, voert de compiler veel transformaties uit om elke taak te voltooien. Sommige instructies kunnen werk initiëren en een Task-object retourneren dat het lopende werk vertegenwoordigt en de compiler moet deze transformaties oplossen. Het doel van het asynchrone programmeren van taken is het inschakelen van code die als een reeks instructies leest, maar in een complexere volgorde wordt uitgevoerd. Uitvoering is gebaseerd op de toewijzing van externe resources en wanneer taken zijn voltooid.
Het asynchrone programmeermodel voor taken is vergelijkbaar met hoe mensen instructies geven voor processen die asynchrone taken bevatten. In dit artikel wordt een voorbeeld gebruikt met instructies voor het maken van ontbijt om te laten zien hoe de async
en await
trefwoorden het gemakkelijker maken om te redeneren over code die een reeks asynchrone instructies bevat. De instructies voor het maken van een ontbijt kunnen worden verstrekt als een lijst:
- Schenk een kopje koffie in.
- Verwarm een pan en bak vervolgens twee eieren.
- Bak drie plakjes spek.
- Toast twee stukjes brood.
- Spreid boter en jam op de toast.
- Giet een glas sinaasappelsap.
Als u ervaring hebt met koken, kunt u deze instructies asynchroonvoltooien. Je begint met het verwarmen van de pan voor eieren en begint met het bakken van het spek. Je zet het brood in de broodrooster en begin met het koken van de eieren. Bij elke stap van het proces start u een taak en gaat u over naar andere taken die klaar zijn voor uw aandacht.
Koken ontbijt is een goed voorbeeld van asynchroon werk dat niet parallel is. Eén persoon (of thread) kan alle taken afhandelen. Eén persoon kan het ontbijt asynchroon maken door de volgende taak te starten voordat de vorige taak is voltooid. Elke kooktaak verloopt, ongeacht of iemand het proces actief bekijkt. Zodra je begint met het verwarmen van de pan voor de eieren, kun je beginnen met het frituren van het spek. Nadat het spek begint te koken, kun je het brood in het broodrooster zetten.
Voor een parallel algoritme hebt u meerdere personen nodig die koken (of meerdere threads). Eén persoon kookt de eieren, een andere friet het spek, enzovoort. Elke persoon richt zich op één specifieke taak. Elke persoon die kookt (of elke draad) wordt synchroon geblokkeerd terwijl ze wachten tot de huidige taak is voltooid: ontbijtspek klaar om om te draaien, brood klaar om uit de broodrooster te springen, enzovoort.
Houd rekening met dezelfde lijst met synchrone instructies die zijn geschreven als C#-code-instructies:
using System;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class Bacon { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static void Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Egg eggs = FryEggs(2);
Console.WriteLine("eggs are ready");
Bacon bacon = FryBacon(3);
Console.WriteLine("bacon is ready");
Toast toast = ToastBread(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static Toast ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
Task.Delay(3000).Wait();
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static Bacon FryBacon(int slices)
{
Console.WriteLine($"putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
Task.Delay(3000).Wait();
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
Task.Delay(3000).Wait();
Console.WriteLine("Put bacon on plate");
return new Bacon();
}
private static Egg FryEggs(int howMany)
{
Console.WriteLine("Warming the egg pan...");
Task.Delay(3000).Wait();
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
Task.Delay(3000).Wait();
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
Als u deze instructies als een computer zou interpreteren, duurt het ongeveer 30 minuten om het ontbijt voor te bereiden. De duur is de som van de afzonderlijke taaktijden. De computer blokkeert voor elke instructie totdat alle werkzaamheden zijn voltooid en gaat vervolgens verder met de volgende taakinstructie. Deze aanpak kan veel tijd in beslag nemen. In het ontbijtvoorbeeld creëert de computermethode een onbevredigend ontbijt. Latere taken in de synchrone lijst, zoals het toasten van het brood, beginnen pas wanneer eerdere taken zijn voltooid. Sommige gerechten worden koud voordat het ontbijt klaar is om te serveren.
Als u wilt dat de computer asynchroon instructies uitvoert, moet u asynchrone code schrijven. Wanneer u clientprogramma's schrijft, wilt u dat de gebruikersinterface reageert op gebruikersinvoer. Uw toepassing mag niet alle interactie blokkeren tijdens het downloaden van gegevens van het web. Wanneer u serverprogramma's schrijft, wilt u threads die mogelijk andere aanvragen verwerken, niet blokkeren. Het gebruik van synchrone code wanneer er asynchrone alternatieven bestaan, beperkt uw vermogen om minder duur uit te breiden. U betaalt voor geblokkeerde threads.
Voor succesvolle moderne apps is asynchrone code vereist. Zonder taalondersteuning vereist het schrijven van asynchrone code callbacks, voltooiingsevenementen of andere middelen waarmee de oorspronkelijke intentie van de code wordt verborgen. Het voordeel van synchrone code is de stapsgewijze actie waarmee u eenvoudig kunt scannen en begrijpen. Traditionele asynchrone modellen dwingen u zich te richten op de asynchrone aard van de code, niet op de fundamentele acties van de code.
Niet blokkeren, wachten in plaats daarvan
De vorige code markeert een ongelukkige programmeerpraktijk: synchrone code schrijven om asynchrone bewerkingen uit te voeren. De code blokkeert dat de huidige thread andere werkzaamheden uitvoert. De code onderbreekt de thread niet terwijl er taken worden uitgevoerd. Het resultaat van dit model is vergelijkbaar met staren op het broodrooster nadat u het brood hebt geplaatst. U negeert eventuele onderbrekingen en start geen andere taken totdat het brood verschijnt. Je haalt de boter en jam niet uit de koelkast. Misschien mis je het zien van een brand die op het fornuis begint. U wilt zowel het brood toasten als andere taken gelijktijdig afhandelen. Hetzelfde geldt voor uw code.
U kunt beginnen met het bijwerken van de code, zodat de thread niet blokkeert terwijl taken worden uitgevoerd. Het trefwoord await
biedt een niet-blokkerende manier om een taak te starten en vervolgens door te gaan met de uitvoering wanneer de taak is voltooid. Een eenvoudige asynchrone versie van de ontbijtcode ziet eruit als het volgende fragment:
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Egg eggs = await FryEggsAsync(2);
Console.WriteLine("eggs are ready");
Bacon bacon = await FryBaconAsync(3);
Console.WriteLine("bacon is ready");
Toast toast = await ToastBreadAsync(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
De code werkt de oorspronkelijke methodeteksten van FryEggs
, FryBacon
en ToastBread
bij om respectievelijk Task<Egg>
, Task<Bacon>
en Task<Toast>
objecten te retourneren. De bijgewerkte methodenamen bevatten het achtervoegsel 'Async': FryEggsAsync
, FryBaconAsync
en ToastBreadAsync
. De methode Main
retourneert het Task
-object, hoewel het geen return
-expressie heeft. Dit is standaard. Zie Evaluatie van een ongeldige asynchrone functievoor meer informatie.
Notitie
De bijgewerkte code maakt nog geen gebruik van de belangrijkste functies van asynchrone programmering, wat kan leiden tot kortere voltooiingstijden. De code verwerkt de taken ongeveer in ongeveer dezelfde tijd als de initiële synchrone versie. Zie de definitieve versie van de code verderop in dit artikel voor de volledige implementatiemethode.
Laten we het voorbeeld van het ontbijt toepassen op de bijgewerkte code. De thread blokkeert niet terwijl de eieren of spek worden gekookt, maar de code start ook geen andere taken totdat het huidige werk is voltooid. Je doet nog steeds het brood in het broodrooster en je staart naar het broodrooster totdat het brood opduikt. Maar je kunt nu reageren op onderbrekingen. In een restaurant waar meerdere bestellingen worden geplaatst, kan de kok een nieuwe bestelling starten terwijl een andere al kookt.
In de bijgewerkte code wordt de thread, die zich bezighoudt met het bereiden van het ontbijt, niet geblokkeerd terwijl wordt gewacht op een gestarte taak die nog niet is voltooid. Voor sommige toepassingen is deze wijziging alles wat u nodig hebt. U kunt uw app inschakelen om gebruikersinteractie te ondersteunen terwijl gegevens van het web worden gedownload. In andere scenario's wilt u mogelijk andere taken starten terwijl u wacht tot de vorige taak is voltooid.
Taken tegelijkertijd starten
Voor de meeste bewerkingen wilt u direct verschillende onafhankelijke taken starten. Wanneer elke taak is voltooid, start u ander werk dat klaar is om te beginnen. Wanneer u deze methodologie toepast op het ontbijtvoorbeeld, kunt u het ontbijt sneller voorbereiden. Je zorgt er ook voor dat alles op ongeveer hetzelfde moment klaar is, zodat je kunt genieten van een warm ontbijt.
De System.Threading.Tasks.Task klasse en gerelateerde typen zijn klassen die u kunt gebruiken om deze stijl van redenering toe te passen op taken die worden uitgevoerd. Met deze benadering kunt u code schrijven die meer lijkt op de manier waarop u ontbijt in het echte leven maakt. Je begint met het koken van eieren, spek en toast op hetzelfde moment. Aangezien elk voedselitem actie vereist, zet u uw aandacht op die taak, zorgt u voor de actie en wacht u vervolgens op iets anders dat uw aandacht vereist.
In uw code start u een taak en houdt u het Task object vast dat het werk vertegenwoordigt. U gebruikt de methode await
voor de taak om de werking van het werk uit te stellen totdat het resultaat gereed is.
Pas deze wijzigingen toe op de ontbijtcode. De eerste stap is het opslaan van de taken voor bewerkingen wanneer ze worden gestart, in plaats van de await
-expressie te gebruiken:
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
Task<Bacon> baconTask = FryBaconAsync(3);
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is ready");
Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Console.WriteLine("Breakfast is ready!");
Deze revisies helpen niet om uw ontbijt sneller klaar te maken. De await
-expressie wordt toegepast op alle taken zodra ze worden gestart. De volgende stap is het verplaatsen van de await
expressies voor spek en eieren naar het einde van de methode, voordat u het ontbijt serveert:
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Task<Bacon> baconTask = FryBaconAsync(3);
Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is ready");
Console.WriteLine("Breakfast is ready!");
U hebt nu een asynchroon bereid ontbijt dat ongeveer 20 minuten duurt om voor te bereiden. De totale kooktijd wordt verminderd omdat sommige taken gelijktijdig worden uitgevoerd.
De code-updates verbeteren het voorbereidingsproces door de kooktijd te verminderen, maar ze introduceren een regressie door de eieren en spek te verbranden. Je start alle asynchrone taken tegelijkertijd. U wacht alleen op elke taak wanneer u de resultaten nodig hebt. De code kan vergelijkbaar zijn met programma's in een webtoepassing die aanvragen doet naar verschillende microservices en vervolgens de resultaten combineert tot één pagina. U voert alle aanvragen onmiddellijk uit en past vervolgens de await
-expressie toe op al deze taken en stelt de webpagina samen.
Ondersteuning bij het samenstellen van taken
De vorige coderevisies helpen alles op hetzelfde moment klaar te maken voor ontbijt, behalve de toast. Het proces van het maken van de toast is een samenstelling van een asynchrone bewerking (het brood toasten) met synchrone bewerkingen (boter en jam op de toast smeren). In dit voorbeeld ziet u een belangrijk concept over asynchrone programmering:
Belangrijk
De samenstelling van een asynchrone bewerking, gevolgd door synchrone werkzaamheden, is een asynchrone bewerking. Anders gezegd, als een deel van een bewerking asynchroon is, is de gehele bewerking asynchroon.
In de vorige updates hebt u geleerd hoe u Task of Task<TResult> objecten kunt gebruiken om taken uit te voeren. U wacht op elke taak voordat u het resultaat ervan gebruikt. De volgende stap bestaat uit het maken van methoden die de combinatie van ander werk vertegenwoordigen. Voordat u het ontbijt serveert, wilt u wachten op de taak van het roosteren van het brood voordat u de boter en jam besmeert.
U kunt dit werk weergeven met de volgende code:
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
De methode MakeToastWithButterAndJamAsync
heeft de async
wijzigingsfunctie in de handtekening die aangeeft aan de compiler dat de methode een await
-expressie bevat en asynchrone bewerkingen bevat. De methode vertegenwoordigt de taak waarmee het brood wordt geroosterd en vervolgens de boter en jam wordt gespreid. De methode retourneert een Task<TResult>-object dat de samenstelling van de drie bewerkingen vertegenwoordigt.
Het herziene hoofdblok met code ziet er nu als volgt uit:
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
var eggsTask = FryEggsAsync(2);
var baconTask = FryBaconAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var eggs = await eggsTask;
Console.WriteLine("eggs are ready");
var bacon = await baconTask;
Console.WriteLine("bacon is ready");
var toast = await toastTask;
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
Deze codewijziging illustreert een belangrijke techniek voor het werken met asynchrone code. Je stelt taken samen door de bewerkingen op te splitsen in een nieuwe methode die een taak retourneert. U kunt kiezen wanneer u wilt wachten op die taak. U kunt andere taken gelijktijdig starten.
Asynchrone uitzonderingen verwerken
Tot nu toe wordt in uw code impliciet ervan uitgegaan dat alle taken zijn voltooid. Asynchrone methoden genereren uitzonderingen, net zoals hun synchrone tegenhangers. De doelen voor asynchrone ondersteuning voor uitzonderingen en foutafhandeling zijn hetzelfde als voor asynchrone ondersteuning in het algemeen. De aanbevolen procedure is om code te schrijven die als een reeks synchrone instructies leest. Taken genereren uitzonderingen wanneer ze niet kunnen worden voltooid. De clientcode kan deze uitzonderingen ondervangen wanneer de await
-expressie wordt toegepast op een gestarte taak.
Stel dat in het ontbijtvoorbeeld de broodrooster brandt terwijl het brood wordt geroosterd. U kunt dit probleem simuleren door de ToastBreadAsync
methode te wijzigen zodat deze overeenkomt met de volgende code:
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(2000);
Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");
await Task.Delay(1000);
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
Notitie
Wanneer u deze code compileert, ziet u een waarschuwing over onbereikbare code. Deze fout is standaard. Nadat de broodrooster brandt, gaan bewerkingen niet normaal verder en retourneert de code een fout.
Nadat u de codewijzigingen hebt aangebracht, voert u de toepassing uit en controleert u de uitvoer:
Pouring coffee
Coffee is ready
Warming the egg pan...
putting 3 slices of bacon in the pan
Cooking first side of bacon...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
Flipping a slice of bacon
Flipping a slice of bacon
Flipping a slice of bacon
Cooking the second side of bacon...
Cracking 2 eggs
Cooking the eggs ...
Put bacon on plate
Put eggs on plate
Eggs are ready
Bacon is ready
Unhandled exception. System.InvalidOperationException: The toaster is on fire
at AsyncBreakfast.Program.ToastBreadAsync(Int32 slices) in Program.cs:line 65
at AsyncBreakfast.Program.MakeToastWithButterAndJamAsync(Int32 number) in Program.cs:line 36
at AsyncBreakfast.Program.Main(String[] args) in Program.cs:line 24
at AsyncBreakfast.Program.<Main>(String[] args)
U ziet dat er heel wat taken zijn voltooid tussen het moment waarop de broodrooster brandt en het systeem de uitzondering waarneemt. Wanneer een taak die asynchroon wordt uitgevoerd een uitzondering genereert, wordt die taak defect. Het Task
object bevat de uitzondering die is opgetreden in de eigenschap Task.Exception. Foutieve taken genereren een uitzondering wanneer de await
-expressie wordt toegepast op de taak.
Er zijn twee belangrijke mechanismen om inzicht te krijgen in dit proces:
- Hoe een uitzondering wordt opgeslagen in een foutieve taak
- Hoe een uitzondering wordt uitgepakt en opnieuw wordt uitgevoerd wanneer code wacht (
await
) op een foutieve taak
Wanneer code die asynchroon wordt uitgevoerd, een uitzondering genereert, wordt de uitzondering opgeslagen in het Task
-object. De eigenschap Task.Exception is een System.AggregateException object, omdat er mogelijk meer dan één uitzondering wordt gegenereerd tijdens asynchroon werk. Een uitzondering wordt aan de AggregateException.InnerExceptions-verzameling toegevoegd. Als de eigenschap Exception
null is, wordt er een nieuw AggregateException
object gemaakt en is de gegenereerde uitzondering het eerste item in de verzameling.
Het meest voorkomende scenario voor een foutieve taak is dat de eigenschap Exception
precies één uitzondering bevat. Wanneer uw code wacht op een foutieve taak, wordt de eerste AggregateException.InnerExceptions-exceptie in de verzameling opnieuw geworpen. Dit resultaat is de reden waarom in de uitvoer uit het voorbeeld een System.InvalidOperationException object wordt weergegeven in plaats van een AggregateException
-object. Door de eerste interne uitzondering te extraheren, wordt werken met asynchrone methoden zo veel mogelijk vergelijkbaar met werken met hun synchrone tegenhangers. U kunt de eigenschap Exception
in uw code onderzoeken wanneer uw scenario meerdere uitzonderingen kan genereren.
Aanbeveling
De aanbevolen procedure is dat eventuele argumentvalidatieuitzonderingen synchroon voortkomen uit methoden die taken retourneren. Zie Uitzonderingen in methoden voor het retourneren van takenvoor meer informatie en voorbeelden.
Voordat u doorgaat naar de volgende sectie, moet u de volgende twee instructies in uw ToastBreadAsync
-methode uitcommentariëren. Je wilt geen andere brand starten:
Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");
Await-expressies efficiënt toepassen op taken
U kunt de reeks await
expressies aan het einde van de vorige code verbeteren met behulp van methoden van de Task
-klasse. Eén API is de WhenAll methode, die een Task object retourneert dat wordt voltooid wanneer alle taken in de lijst met argumenten zijn voltooid. De volgende code demonstreert deze methode:
await Task.WhenAll(eggsTask, baconTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Bacon is ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");
Een andere optie is om de methode WhenAny te gebruiken, die een Task<Task>
object retourneert dat wordt voltooid wanneer een van de argumenten is voltooid. U kunt wachten op de geretourneerde taak omdat u weet dat de taak is voltooid. De volgende code laat zien hoe u de methode WhenAny kunt gebruiken om te wachten op de eerste taak om te voltooien en vervolgens het resultaat te verwerken. Nadat u het resultaat van de voltooide taak hebt verwerkt, verwijdert u de voltooide taak uit de lijst met taken die zijn doorgegeven aan de WhenAny
-methode.
var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("Eggs are ready");
}
else if (finishedTask == baconTask)
{
Console.WriteLine("Bacon is ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("Toast is ready");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
Aan het einde van het codefragment ziet u de await finishedTask;
-expressie. De await Task.WhenAny
-expressie wacht niet op de voltooide taak, maar wacht op het Task
object dat wordt geretourneerd door de Task.WhenAny
methode. Het resultaat van de Task.WhenAny
methode is de voltooide (of defecte) taak. De aanbevolen procedure is om nogmaals op de taak te wachten, zelfs wanneer u weet dat de taak is voltooid. Op deze manier kunt u het taakresultaat ophalen of ervoor zorgen dat een uitzondering wordt opgeworpen wanneer de taak een fout veroorzaakt.
Definitieve code controleren
De uiteindelijke versie van de code ziet er als volgt uit:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class Bacon { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
var eggsTask = FryEggsAsync(2);
var baconTask = FryBaconAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("eggs are ready");
}
else if (finishedTask == baconTask)
{
Console.WriteLine("bacon is ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("toast is ready");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(3000);
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static async Task<Bacon> FryBaconAsync(int slices)
{
Console.WriteLine($"putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
await Task.Delay(3000);
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
await Task.Delay(3000);
Console.WriteLine("Put bacon on plate");
return new Bacon();
}
private static async Task<Egg> FryEggsAsync(int howMany)
{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
De code voltooit de asynchrone ontbijttaken in ongeveer 15 minuten. De totale tijd wordt verminderd omdat sommige taken gelijktijdig worden uitgevoerd. De code bewaakt tegelijkertijd meerdere taken en neemt alleen actie indien nodig.
De laatste code is asynchroon. Het weerspiegelt nauwkeuriger hoe een persoon het ontbijt kan koken. Vergelijk de laatste code met het eerste codevoorbeeld in het artikel. De belangrijkste acties zijn nog steeds duidelijk door de code te lezen. U kunt de laatste code op dezelfde manier lezen als u de lijst met instructies voor het maken van een ontbijt leest, zoals wordt weergegeven aan het begin van het artikel. De taalfuncties voor de async
en await
trefwoorden verschaffen de vertaling die iedere persoon maakt om de geschreven instructies te volgen: Start taken zodra u dat kunt en blokkeer niet terwijl u wacht totdat taken zijn voltooid.
Async/await versus ContinueWith
De async
trefwoorden en await
trefwoorden bieden syntactische vereenvoudiging ten opzichte van het rechtstreeks gebruiken Task.ContinueWith . Hoewel async
/await
en ContinueWith
vergelijkbare semantiek hebben voor het verwerken van asynchrone bewerkingen, vertaalt await
de compiler expressies niet noodzakelijkerwijs rechtstreeks naar ContinueWith
methodeaanroepen. In plaats daarvan genereert de compiler geoptimaliseerde statuscomputercode die hetzelfde logische gedrag biedt. Deze transformatie biedt aanzienlijke leesbaarheid en onderhoudbaarheidsvoordelen, met name bij het koppelen van meerdere asynchrone bewerkingen.
Overweeg een scenario waarin u meerdere opeenvolgende asynchrone bewerkingen moet uitvoeren. Hier ziet u hoe dezelfde logica eruitziet wanneer deze wordt geïmplementeerd in vergelijking metContinueWith
/async
await
:
ContinueWith gebruiken
Voor ContinueWith
elke stap in een reeks asynchrone bewerkingen zijn geneste vervolgbewerkingen vereist:
// Using ContinueWith - demonstrates the complexity when chaining operations
static Task MakeBreakfastWithContinueWith()
{
return StartCookingEggsAsync()
.ContinueWith(eggsTask =>
{
var eggs = eggsTask.Result;
Console.WriteLine("Eggs ready, starting bacon...");
return StartCookingBaconAsync();
})
.Unwrap()
.ContinueWith(baconTask =>
{
var bacon = baconTask.Result;
Console.WriteLine("Bacon ready, starting toast...");
return StartToastingBreadAsync();
})
.Unwrap()
.ContinueWith(toastTask =>
{
var toast = toastTask.Result;
Console.WriteLine("Toast ready, applying butter...");
return ApplyButterAsync(toast);
})
.Unwrap()
.ContinueWith(butteredToastTask =>
{
var butteredToast = butteredToastTask.Result;
Console.WriteLine("Butter applied, applying jam...");
return ApplyJamAsync(butteredToast);
})
.Unwrap()
.ContinueWith(finalToastTask =>
{
var finalToast = finalToastTask.Result;
Console.WriteLine("Breakfast completed with ContinueWith!");
});
}
Async/await gebruiken
Dezelfde reeks bewerkingen met gebruik van async
/await
leest veel natuurlijker.
// Using async/await - much cleaner and easier to read
static async Task MakeBreakfastWithAsyncAwait()
{
var eggs = await StartCookingEggsAsync();
Console.WriteLine("Eggs ready, starting bacon...");
var bacon = await StartCookingBaconAsync();
Console.WriteLine("Bacon ready, starting toast...");
var toast = await StartToastingBreadAsync();
Console.WriteLine("Toast ready, applying butter...");
var butteredToast = await ApplyButterAsync(toast);
Console.WriteLine("Butter applied, applying jam...");
var finalToast = await ApplyJamAsync(butteredToast);
Console.WriteLine("Breakfast completed with async/await!");
}
Waarom async/await de voorkeur heeft
De async
/await
aanpak biedt verschillende voordelen:
- Leesbaarheid: De code leest als synchrone code, waardoor de stroom van bewerkingen gemakkelijker te begrijpen is.
- Onderhoudbaarheid: voor het toevoegen of verwijderen van stappen in de reeks zijn minimale codewijzigingen vereist.
-
Foutafhandeling: uitzonderingsafhandeling met
try
/catch
blokken werkt natuurlijk, terwijlContinueWith
zorgvuldige verwerking van mislukte taken vereist is. -
Foutopsporing: de ervaring van de aanroepstack en het foutopsporingsprogramma is veel beter met
async
/await
. -
Prestaties: de compileroptimalisaties zijn
async
/await
geavanceerder dan handmatigeContinueWith
ketens.
Het voordeel wordt nog duidelijker naarmate het aantal ketenbewerkingen toeneemt. Hoewel één voortzetting beheerbaar kan zijn, ContinueWith
worden reeksen van 3-4 of meer asynchrone bewerkingen snel moeilijk te lezen en te onderhouden. Met dit patroon, ook wel 'monadic do-notation' genoemd in functionele programmering, kunt u meerdere asynchrone bewerkingen op een sequentiële, leesbare manier opstellen.