Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Das Task-asynchrone Programmierungsmodell (TAP) bietet eine Abstraktionsebene über typischen asynchronen Code. In diesem Modell schreiben Sie Code als Abfolge von Anweisungen, die wie gewohnt identisch sind. Der Unterschied besteht darin, dass Sie den aufgabenbasierten Code lesen können, während der Compiler jede Anweisung verarbeitet und bevor sie mit der Verarbeitung der nächsten Anweisung beginnt. Um dieses Modell zu erreichen, führt der Compiler viele Transformationen aus, um jede Aufgabe abzuschließen. Einige Anweisungen können Arbeit initiieren und ein Task Objekt zurückgeben, das die laufende Arbeit darstellt, und der Compiler muss diese Transformationen auflösen. Das Ziel der asynchronen Programmierung von Aufgaben ist das Aktivieren von Code, der wie eine Abfolge von Anweisungen liest, aber in einer komplizierteren Reihenfolge ausgeführt wird. Die Ausführung basiert auf der externen Ressourcenzuordnung und nach Abschluss von Vorgängen.
Das asynchrone Programmiermodell der Aufgabe entspricht der Vorgehensweise, wie Personen Anweisungen für Prozesse mit asynchronen Aufgaben geben. In diesem Artikel wird ein Beispiel mit Anweisungen zur Zubereitung von Frühstück verwendet, um zu zeigen, wie die async- und await-Schlüsselwörter das Verständnis von Code erleichtern, der eine Reihe asynchroner Anweisungen enthält. Die Anweisungen zum Erstellen eines Frühstücks können als Liste bereitgestellt werden:
- Gießen Sie eine Tasse Kaffee ein.
- Wärmen Sie eine Pfanne, und braten Sie dann zwei Eier.
- Kochen Sie drei haschbraune Patties.
- Toasten Sie zwei Stücke Brot.
- Bestreichen Sie den Toast mit Butter und Marmelade.
- Gießen Sie ein Glas Orangensaft.
Wenn Sie Erfahrung mit dem Kochen haben, können Sie diese Anweisungen asynchron ausführen. Sie beginnen, die Pfanne für Eier zu erwärmen und dann mit dem Kochen der Haschbraunen zu beginnen. Sie legen das Brot in den Toaster und beginnen dann mit dem Kochen der Eier. Bei jedem Schritt des Prozesses starten Sie eine Aufgabe und wechseln dann zu anderen Aufgaben, die für Ihre Aufmerksamkeit bereit sind.
Kochfrühstück ist ein gutes Beispiel für asynchrone Arbeit, die nicht parallel ist. Eine Person (oder ein Thread) kann alle Aufgaben verarbeiten. Eine Person kann frühstücken, indem sie die nächste Aufgabe asynchron starten, bevor die vorherige Aufgabe abgeschlossen ist. Jede Kochaufgabe verläuft unabhängig davon, ob jemand aktiv den Prozess überwacht. Sobald Sie mit der Erwärmung der Pfanne für die Eier beginnen, können Sie mit dem Kochen der Haschbraun beginnen. Nachdem die Hashbraunen zu kochen beginnen, können Sie das Brot in den Toaster legen.
Für einen parallelen Algorithmus benötigen Sie mehrere Personen, die kochen (oder mehrere Threads). Eine Person kocht die Eier, ein anderer kocht die Haschbraun usw. Jede Person konzentriert sich auf ihre spezifische Aufgabe. Jede Person, die kocht (oder jeder Thread) wird synchron blockiert, bis die aktuelle Aufgabe abgeschlossen ist: Hashbraunen bereit zum Kippen, Brot bereit, um im Toaster aufzufüllen usw.
Betrachten Sie dieselbe Liste synchroner Anweisungen, die als C#-Codeanweisungen geschrieben wurden:
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 HashBrown { }
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");
HashBrown hashBrown = FryHashBrowns(3);
Console.WriteLine("hash browns are 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 HashBrown FryHashBrowns(int patties)
{
Console.WriteLine($"putting {patties} hash brown patties in the pan");
Console.WriteLine("cooking first side of hash browns...");
Task.Delay(3000).Wait();
for (int patty = 0; patty < patties; patty++)
{
Console.WriteLine("flipping a hash brown patty");
}
Console.WriteLine("cooking the second side of hash browns...");
Task.Delay(3000).Wait();
Console.WriteLine("Put hash browns on plate");
return new HashBrown();
}
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();
}
}
}
Wenn Sie diese Anweisungen wie ein Computer interpretieren würden, dauert die Zubereitung des Frühstücks etwa 30 Minuten. Die Dauer ist die Summe der einzelnen Vorgangszeiten. Der Computer blockiert für jede Anweisung, bis alle Arbeiten abgeschlossen sind, und fährt dann mit der nächsten Vorgangsanweisung fort. Dieser Ansatz kann erhebliche Zeit in Anspruch nehmen. Im Frühstücksbeispiel erstellt die Computermethode ein unbefriedigendes Frühstück. Spätere Aufgaben in der synchronen Liste, wie das Toasten des Brotes, beginnen erst, wenn frühere Aufgaben abgeschlossen sind. Manche Speisen werden kalt, bevor das Frühstück servierbereit ist.
Wenn der Computer Anweisungen asynchron ausführen soll, müssen Sie asynchronen Code schreiben. Wenn Sie Clientprogramme schreiben, soll die Benutzeroberfläche für Benutzereingaben reaktionsfähig sein. Ihre Anwendung sollte nicht jegliche Interaktion blockieren, während die Anwendung Daten aus dem Web herunterlädt. Wenn Sie Serverprogramme schreiben, möchten Sie keine Threads blockieren, die möglicherweise andere Anforderungen erfüllen. Die Verwendung synchroner Codes, wenn asynchrone Alternativen existieren, beeinträchtigt Ihre Fähigkeit, kostengünstiger zu skalieren. Sie bezahlen für blockierte Threads.
Erfolgreiche moderne Apps erfordern asynchronen Code. Ohne Sprachunterstützung erfordert das Schreiben von asynchronem Code Rückrufe, Abschlussereignisse oder andere Mittel, die die ursprüngliche Absicht des Codes verdecken. Der Vorteil synchroner Code ist die schritt-für-Schritt-Aktion, die das Scannen und Verstehen erleichtert. Herkömmliche asynchrone Modelle zwingen Sie, sich auf die asynchrone Art des Codes zu konzentrieren, nicht auf die grundlegenden Aktionen des Codes.
Blockieren Sie nicht, warten Sie stattdessen
Im vorherigen Code wird eine unglückliche Programmierpraxis hervorgehoben: Schreiben von synchronen Code zum Ausführen asynchroner Vorgänge. Mit dem Code wird verhindert, dass der aktuelle Thread andere Aufgaben ausführt. Der Code unterbricht den Thread nicht, während Aufgaben ausgeführt werden. Das Ergebnis dieses Modells ähnelt dem Anstarren des Toasters, nachdem Sie das Brot gelegt haben. Du ignorierst alle Unterbrechungen und beginnst keine anderen Aufgaben, bis das Brot hochpoppt. Sie nehmen weder die Butter noch die Marmelade aus dem Kühlschrank. Möglicherweise verpassen Sie ein Feuer, das auf dem Herd beginnt. Sie wollen das Brot toasten und sich gleichzeitig um andere Dinge kümmern. Dasselbe gilt für Ihren Code.
Sie können damit beginnen, den Code zu aktualisieren, damit der Thread nicht blockiert wird, während Aufgaben ausgeführt werden. Das await Schlüsselwort bietet eine nicht blockierende Möglichkeit, eine Aufgabe zu starten und dann die Ausführung fortzusetzen, wenn die Aufgabe abgeschlossen ist. Eine einfache asynchrone Version des Frühstückscodes sieht wie der folgende Codeausschnitt aus:
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Egg eggs = await FryEggsAsync(2);
Console.WriteLine("eggs are ready");
HashBrown hashBrown = await FryHashBrownsAsync(3);
Console.WriteLine("hash browns are 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!");
}
Der Code aktualisiert die ursprünglichen Methodenkörper von FryEggs, FryHashBrowns und ToastBread, um Task<Egg>, Task<HashBrown> und Task<Toast>-Objekte zurückzugeben. Die aktualisierten Methodennamen enthalten das Suffix "Async": FryEggsAsync, , FryHashBrownsAsync, und ToastBreadAsync. Die Main-Methode gibt das Task-Objekt zurück, obwohl sie keinen return-Ausdruck hat, was so beabsichtigt ist. Weitere Informationen finden Sie unter Bewertung einer asynchronen Funktion mit Rückgabewert "void".
Hinweis
Der aktualisierte Code nutzt noch nicht die wichtigsten Features der asynchronen Programmierung, was zu kürzeren Vervollständigungszeiten führen kann. Der Code verarbeitet die Aufgaben in etwa der gleichen Zeit wie die ursprüngliche synchrone Version. Die vollständigen Methodenimplementierungen finden Sie in der endgültigen Version des Codes weiter unten in diesem Artikel.
Lassen Sie uns das Frühstücksbeispiel auf den aktualisierten Code anwenden. Der Thread blockiert nicht, während die Eier oder Hashbraunen kochen, aber der Code startet auch nicht andere Aufgaben, bis die aktuelle Arbeit abgeschlossen ist. Sie legen das Brot immer noch in den Toaster und starren den Toaster an, bis das Brot hochkommt, aber jetzt können Sie auf Unterbrechungen reagieren. In einem Restaurant, in dem mehrere Bestellungen aufgegeben werden, kann der Koch eine neue Bestellung beginnen, während eine andere bereits kocht.
Im aktualisierten Code wird der Thread, der am Frühstücksprozess arbeitet, nicht blockiert, während er auf eine gestartete Aufgabe wartet, die unvollendet ist. Bei einigen Anwendungen ist diese Änderung alles, was Sie benötigen. Sie können Ihre App aktivieren, um die Benutzerinteraktion zu unterstützen, während Datendownloads aus dem Web heruntergeladen werden. In anderen Szenarien möchten Sie möglicherweise andere Aufgaben starten, während Sie auf den Abschluss der vorherigen Aufgabe warten.
Gleichzeitiges Starten von Aufgaben
Für die meisten Vorgänge möchten Sie mehrere unabhängige Vorgänge sofort starten. Sobald jede Aufgabe abgeschlossen ist, initiieren Sie andere Arbeiten, die zum Starten bereit sind. Wenn Sie diese Methodik auf das Frühstücksbeispiel anwenden, können Sie das Frühstück schneller vorbereiten. Sie bereiten alles rechtzeitig vor, damit Sie ein warmes Frühstück genießen können.
Bei den System.Threading.Tasks.Task Klassen und verwandten Typen handelt es sich um Klassen, die Sie verwenden können, um diese Art der Begründung auf Aufgaben anzuwenden, die gerade ausgeführt werden. Mit diesem Ansatz können Sie Code schreiben, der der Art und Weise ähnelt, wie Sie Frühstück im realen Leben erstellen. Sie beginnen, die Eier zu kochen, Haschbraun und Popup gleichzeitig zu kochen. Da jedes Lebensmittel eine Aktion erfordert, wenden Sie ihre Aufmerksamkeit auf diese Aufgabe, kümmern Sie sich um die Aktion, und warten Sie dann auf etwas anderes, das Ihre Aufmerksamkeit erfordert.
In Ihrem Code starten Sie eine Aufgabe und halten sie an das Task Objekt fest, das die Arbeit darstellt. Sie verwenden die await Methode für die Aufgabe, um die Bearbeitung der Aufgabe zu verzögern, bis das Ergebnis fertig ist.
Wenden Sie diese Änderungen auf den Frühstückscode an. Der erste Schritt besteht darin, die Aufgaben für Vorgänge beim Start zu speichern, anstatt den await Ausdruck zu verwenden:
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are 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!");
Diese Überarbeitungen helfen nicht, Ihr Frühstück schneller zu machen. Der await Ausdruck wird auf alle Aufgaben angewendet, sobald sie beginnen. Der nächste Schritt besteht darin, die await Ausdrücke für die Hashbraunen und Eier bis zum Ende der Methode zu verschieben, bevor Sie das Frühstück serviert haben:
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Task<HashBrown> hashBrownTask = FryHashBrownsAsync(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");
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are ready");
Console.WriteLine("Breakfast is ready!");
Sie haben nun ein asynchron zubereitetes Frühstück, dessen Zubereitung etwa 20 Minuten dauert. Die Gesamtkochzeit wird reduziert, da einige Aufgaben gleichzeitig ausgeführt werden.
Der Code aktualisiert den Vorbereitungsprozess, indem er die Kochzeit reduziert, aber sie führen eine Regression durch Brennen der Eier und Haschbraunen ein. Sie starten alle asynchronen Aufgaben gleichzeitig. Sie warten nur auf jeden Vorgang, wenn Sie die Ergebnisse benötigen. Der Code ähnelt möglicherweise dem Programm in einer Webanwendung, die Anforderungen an verschiedene Microservices sendet, und kombiniert dann die Ergebnisse in einer einzelnen Seite. Sie machen alle Anfragen sofort, wenden dann den await Ausdruck auf all diese Aufgaben an und erstellen die Webseite.
Komposition mit Aufgaben unterstützen
Die vorherigen Codeänderungen helfen, alles für das Frühstück gleichzeitig vorzubereiten, außer dem Toast. Der Prozess der Herstellung des Toasts ist eine Zusammensetzung eines asynchronen Vorgangs (das Brot toasten) mit synchronen Vorgängen (Butter und Marmelade auf dem Toast verteilen). In diesem Beispiel wird ein wichtiges Konzept für die asynchrone Programmierung veranschaulicht:
Von Bedeutung
Die Kombination aus einem asynchronen Vorgang gefolgt von einer synchronen Tätigkeit ergibt einen asynchronen Vorgang. Anders ausgedrückt: Wenn irgendein Teil eines Vorgangs asynchron ist, ist der gesamte Vorgang asynchron.
In den vorherigen Updates haben Sie erfahren, wie Sie Task- oder Task<TResult>-Objekte verwenden, um laufende Aufgaben beizubehalten. Sie warten auf jeden Vorgang, bevor Sie dessen Ergebnis verwenden. Der nächste Schritt besteht darin, Methoden zu erstellen, die die Kombination anderer Arbeiten darstellen. Vor dem Servieren des Frühstücks sollten Sie auf die Aufgabe warten, mit der das Brot getoastet wird, bevor Sie es mit Butter und Marmelade bestreichen.
Sie können diese Arbeit mit dem folgenden Code darstellen:
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
Die MakeToastWithButterAndJamAsync Methode weist den async Modifizierer in seiner Signatur auf, der dem Compiler signalisiert, dass die Methode einen await Ausdruck enthält und asynchrone Vorgänge enthält. Die Methode stellt die Aufgabe dar, das Brot zu toasten und dann die Butter und Marmelade zu verteilen. Die Methode gibt ein Task<TResult> Objekt zurück, das die Zusammensetzung der drei Vorgänge darstellt.
Der überarbeitete Hauptcodeblock sieht nun wie folgt aus:
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
var eggsTask = FryEggsAsync(2);
var hashBrownTask = FryHashBrownsAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var eggs = await eggsTask;
Console.WriteLine("eggs are ready");
var hashBrown = await hashBrownTask;
Console.WriteLine("hash browns are ready");
var toast = await toastTask;
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
Diese Codeänderung veranschaulicht eine wichtige Technik für die Arbeit mit asynchronen Code. Sie verfassen Aufgaben, indem Sie die Vorgänge in eine neue Methode trennen, die einen Vorgang zurückgibt. Sie können auswählen, wann sie auf diese Aufgabe warten sollen. Sie können andere Aufgaben gleichzeitig starten.
Asynchrone Ausnahmen behandeln
Bis zu diesem Punkt geht Ihr Code implizit davon aus, dass alle Aufgaben erfolgreich abgeschlossen wurden. Asynchrone Methoden lösen Ausnahmen aus, genau wie ihre synchronen Gegenstücke. Die Ziele für asynchrone Unterstützung für Ausnahmen und Fehlerbehandlung sind identisch mit der asynchronen Unterstützung im Allgemeinen. Die bewährte Methode besteht darin, Code zu schreiben, der wie eine Reihe synchroner Anweisungen liest. Aufgaben lösen Ausnahmen aus, wenn sie nicht erfolgreich abgeschlossen werden können. Der Clientcode kann diese Ausnahmen abfangen, wenn der await Ausdruck auf eine gestartete Aufgabe angewendet wird.
Nehmen Sie im Frühstücksbeispiel an, dass der Toaster in Flammen aufgeht, während das Brot getoastet wird. Sie können dieses Problem simulieren, indem Sie die ToastBreadAsync Methode so ändern, dass sie mit dem folgenden Code übereinstimmt:
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();
}
Hinweis
Beim Kompilieren dieses Codes wird eine Warnung zu nicht erreichbarem Code angezeigt. Dieser Fehler ist beabsichtigt. Nachdem der Toaster Feuer fängt, werden Vorgänge nicht normal fortgesetzt, und der Code gibt einen Fehler zurück.
Nachdem Sie die Codeänderungen vorgenommen haben, führen Sie die Anwendung aus, und überprüfen Sie die Ausgabe:
Pouring coffee
Coffee is ready
Warming the egg pan...
putting 3 hash brown patties in the pan
Cooking first side of hash browns...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
Flipping a hash brown patty
Flipping a hash brown patty
Flipping a hash brown patty
Cooking the second side of hash browns...
Cracking 2 eggs
Cooking the eggs ...
Put hash browns on plate
Put eggs on plate
Eggs are ready
Hash browns are 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)
Beachten Sie, dass ziemlich viele Aufgaben zwischen dem Zeitpunkt, zu dem der Toaster Feuer fängt, und dem Moment, in dem das System die Ausnahme feststellt, beendet werden. Wenn eine Aufgabe, die asynchron ausgeführt wird, eine Ausnahme auslöst, ist diese Aufgabe fehlerhaft. Das Task Objekt enthält die Ausnahme, die in der Task.Exception Eigenschaft ausgelöst wird. Fehlerhafte Vorgänge lösen eine Ausnahme aus, wenn der await Ausdruck auf die Aufgabe angewendet wird.
Es gibt zwei wichtige Mechanismen, um diesen Prozess zu verstehen:
- Wie eine Ausnahme in einer fehlerhaften Aufgabe gespeichert wird
- Wie eine Ausnahme entpackt und erneut ausgelöst wird, wenn Code auf einen fehlerhaften Vorgang wartet (
await)
Wenn code, der asynchron ausgeführt wird, eine Ausnahme auslöst, wird die Ausnahme im Task Objekt gespeichert. Die Task.Exception-Eigenschaft ist ein System.AggregateException-Objekt, da während asynchronen Vorgängen möglicherweise mehr als eine Ausnahme ausgelöst wird. Jede ausgelöste Ausnahme wird der AggregateException.InnerExceptions Auflistung hinzugefügt. Wenn die Exception Eigenschaft NULL ist, wird ein neues AggregateException Objekt erstellt, und die ausgelöste Ausnahme ist das erste Element in der Auflistung.
Das häufigste Szenario für einen fehlerhaften Vorgang besteht darin, dass die Exception Eigenschaft genau eine Ausnahme enthält. Wenn Ihr Code auf eine fehlerhafte Aufgabe wartet, wird die erste AggregateException.InnerExceptions -Ausnahme in der Sammlung erneut ausgelöst. Dieses Ergebnis ist der Grund, warum die Ausgabe aus dem Beispiel ein System.InvalidOperationException Objekt anstelle eines AggregateException Objekts zeigt. Das Extrahieren der ersten inneren Ausnahme macht das Arbeiten mit asynchronen Methoden so ähnlich wie möglich mit ihren synchronen Gegenstücken. Sie können die Exception Eigenschaft in Ihrem Code untersuchen, wenn ihr Szenario möglicherweise mehrere Ausnahmen generiert.
Tipp
Die empfohlene Praxis ist, dass alle Ausnahmen bei der Argumentvalidierung synchron von Methoden auftreten, die eine Aufgabe zurückgeben. Weitere Informationen und Beispiele finden Sie unter Ausnahmen in Aufgabenrückgabemethoden.
Bevor Sie mit dem nächsten Abschnitt fortfahren, kommentieren Sie die folgenden beiden Anweisungen in Ihrer ToastBreadAsync Methode aus. Sie möchten kein weiteres Feuer starten:
Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");
Effiziente Anwendung von await-Ausdrücken auf Aufgaben
Sie können die Reihe von await Ausdrücken am Ende des vorherigen Codes mithilfe von Methoden der Task Klasse verbessern. Eine API ist die WhenAll Methode, die ein Task Objekt zurückgibt, das abgeschlossen wird, wenn alle Aufgaben in der Argumentliste abgeschlossen sind. Der folgende Code veranschaulicht diese Methode:
await Task.WhenAll(eggsTask, hashBrownTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Hash browns are ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");
Eine weitere Option besteht darin, die WhenAny Methode zu verwenden, die ein Task<Task> Objekt zurückgibt, das abgeschlossen wird, wenn eines seiner Argumente abgeschlossen ist. Sie können auf den zurückgegebenen Vorgang warten, da Sie wissen, dass die Aufgabe abgeschlossen ist. Der folgende Code zeigt, wie Sie die WhenAny Methode verwenden können, um auf den abschluss der ersten Aufgabe zu warten und dann das Ergebnis zu verarbeiten. Nachdem Sie das Ergebnis aus der abgeschlossenen Aufgabe verarbeitet haben, entfernen Sie die abgeschlossene Aufgabe aus der Liste der an die WhenAny Methode übergebenen Aufgaben.
var breakfastTasks = new List<Task> { eggsTask, hashBrownTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("Eggs are ready");
}
else if (finishedTask == hashBrownTask)
{
Console.WriteLine("Hash browns are ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("Toast is ready");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
Beachten Sie am Ende des Codeausschnitts den await finishedTask; Ausdruck. Diese Zeile ist wichtig, da Task.WhenAny eine Task<Task> Wrapperaufgabe zurückgegeben wird, die die abgeschlossene Aufgabe enthält. Wenn Sie await Task.WhenAnydarauf warten, dass die Wrapperaufgabe abgeschlossen ist, und das Ergebnis ist der eigentliche Vorgang, der zuerst abgeschlossen wurde. Um jedoch das Ergebnis dieser Aufgabe abzurufen oder sicherzustellen, dass Ausnahmen ordnungsgemäß ausgelöst werden, müssen await Sie die abgeschlossene Aufgabe selbst (gespeichert in finishedTask) ausführen. Obwohl Sie wissen, dass die Aufgabe abgeschlossen ist, können Sie erneut auf das Ergebnis zugreifen oder ausnahmen behandeln, die möglicherweise zu Einem Fehler geführt haben.
Überprüfen des endgültigen Codes
So sieht die endgültige Version des Codes aus:
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 HashBrown { }
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 hashBrownTask = FryHashBrownsAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var breakfastTasks = new List<Task> { eggsTask, hashBrownTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("eggs are ready");
}
else if (finishedTask == hashBrownTask)
{
Console.WriteLine("hash browns are 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<HashBrown> FryHashBrownsAsync(int patties)
{
Console.WriteLine($"putting {patties} hash brown patties in the pan");
Console.WriteLine("cooking first side of hash browns...");
await Task.Delay(3000);
for (int patty = 0; patty < patties; patty++)
{
Console.WriteLine("flipping a hash brown patty");
}
Console.WriteLine("cooking the second side of hash browns...");
await Task.Delay(3000);
Console.WriteLine("Put hash browns on plate");
return new HashBrown();
}
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();
}
}
}
Der Code führt die asynchronen Frühstücksaufgaben in ca. 15 Minuten aus. Die Gesamtzeit wird reduziert, da einige Aufgaben gleichzeitig ausgeführt werden. Der Code überwacht gleichzeitig mehrere Aufgaben und führt nur bei Bedarf Aktionen aus.
Der letzte Code ist asynchron. Es spiegelt genauer wider, wie eine Person frühstücken könnte. Vergleichen Sie den endgültigen Code mit dem ersten Codebeispiel im Artikel. Die Kernaktionen sind weiterhin klar, wenn man den Code liest. Sie können den endgültigen Code auf die gleiche Weise lesen, wie Sie die Liste der Anweisungen zum Erstellen eines Frühstücks lesen, wie am Anfang des Artikels gezeigt. Die Sprachfunktionen für die async- und await-Schlüsselwörter stellen die Übersetzung bereit, die jede Person vornimmt, um die schriftlichen Anweisungen zu befolgen: Starten Sie Aufgaben, sobald Sie dies können, und blockieren Sie nicht den weiteren Fortgang, während Sie auf den Abschluss von Aufgaben warten.
Async/await vs ContinueWith
Die async und await Schlüsselwörter bieten eine syntaktische Vereinfachung gegenüber der direkten Verwendung von Task.ContinueWith. Während async/await und ContinueWith eine ähnliche Semantik zur Behandlung asynchroner Vorgänge haben, übersetzt der Compiler await nicht unbedingt direkt in ContinueWith-Methodenaufrufe. Stattdessen generiert der Compiler optimierten Zustandsautomatencode, der dasselbe logische Verhalten bereitstellt. Diese Transformation bietet erhebliche Vorteile der Lesbarkeit und Wartung, insbesondere beim Verketten mehrerer asynchroner Vorgänge.
Betrachten Sie ein Szenario, in dem Sie mehrere sequenzielle asynchrone Vorgänge ausführen müssen. So sieht dieselbe Logik aus, wenn sie mit ContinueWith im Vergleich zu async/await implementiert wird:
Verwenden von ContinueWith
Bei ContinueWith, jeder Schritt in einer Folge von asynchronen Operationen erfordert geschachtelte Fortsetzungen:
// 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!");
});
}
Verwenden von async/await
Die gleiche Abfolge von Schritten mit async/await liest sich viel natürlicher:
// 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!");
}
Warum async/await bevorzugt wird
Der async/await Ansatz bietet mehrere Vorteile:
- Lesbarkeit: Der Code liest wie synchronen Code und erleichtert das Verständnis des Ablaufs von Vorgängen.
- Verwendbarkeit: Das Hinzufügen oder Entfernen von Schritten in der Sequenz erfordert minimale Codeänderungen.
-
Fehlerbehandlung: Die Ausnahmebehandlung mit
try/catchBlöcken funktioniert natürlich, währendContinueWitheine sorgfältige Behandlung fehlerhafter Vorgänge erforderlich ist. -
Debuggen: Der Aufrufstapel und das Debugger-Erlebnis sind mit
async/awaitdeutlich verbessert. -
Leistung: Die Optimierungen des Compilers für
async/awaitsind anspruchsvoller als manuelleContinueWithKetten.
Der Vorteil wird noch deutlicher, da die Anzahl der verketteten Vorgänge steigt. Während eine einzelne fortsetzung vielleicht mit ContinueWith überschaubar sein mag, werden Sequenzen von 3-4 oder mehr asynchronen Operationen schnell schwierig zu lesen und zu pflegen. Mit diesem Muster, das in der funktionalen Programmierung als "monadische Do-Notation" bezeichnet wird, können Sie mehrere asynchrone Vorgänge auf sequenzielle und lesbare Weise verfassen.