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.
- Braten Sie drei Scheiben Speck.
- 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 den Bacon zu braten. 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 beginnen, die Pfanne für die Eier zu erwärmen, können Sie beginnen, den Bacon zu braten. Nachdem der Bacon zu kochen beginnt, 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, eine andere brät den Speck, und so weiter. Jede Person konzentriert sich auf ihre spezifische Aufgabe. Jede Person, die kocht (oder jeder Thread) wird synchron blockiert, bis die aktuelle Aufgabe abgeschlossen ist: Speck ist bereit zum Wenden, Brot ist bereit, aus dem Toaster zu springen, und so weiter.
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 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();
}
}
}
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 möchten sowohl das Brot toasten als auch andere Angelegenheiten behandeln. 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");
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!");
}
Der Code aktualisiert die ursprünglichen Methodenkörper von FryEggs
, FryBacon
und ToastBread
, um Task<Egg>
, Task<Bacon>
und Task<Toast>
-Objekte zurückzugeben. Die aktualisierten Methodennamen enthalten das Suffix "Async": FryEggsAsync
, , FryBaconAsync
, 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 Bacon kochen, aber der Code startet auch keine anderen 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 gleichzeitig mit der Zubereitung der Eier, des Specks und des Toasts. 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<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!");
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 den Bacon und die 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<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!");
Sie haben jetzt ein asynchron vorbereitetes Frühstück, das etwa 20 Minuten dauert, um sich vorzubereiten. Die Gesamtkochzeit wird reduziert, da einige Aufgaben gleichzeitig ausgeführt werden.
Der Code aktualisiert den Vorbereitungsprozess, indem er die Kochzeit reduziert, führt jedoch zu einem Rückschritt, da die Eier und der Speck anbrennen. 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 durch 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 Zusammensetzung eines asynchronen Vorgangs gefolgt von synchroner Arbeit ist ein asynchroner 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 zu enthalten. 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. Bevor Sie das Frühstück servieren, möchten Sie auf die Aufgabe warten, die das Brot toastet, bevor Sie Butter und Marmelade streichen.
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 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!");
}
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 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)
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 der asynchronen Arbeit möglicherweise mehrere Ausnahmen ausgelöst werden. 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 Auflistung 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 Argumentvalidierungsausnahmen synchron von aufgabenrückführenden Methoden auftreten. 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 Methode ToastBreadAsync
aus. Sie möchten kein weiteres Feuer starten:
Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");
Effizientes Anwenden 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, baconTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Bacon is 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, 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);
}
Beachten Sie am Ende des Codeausschnitts den await finishedTask;
Ausdruck. Der await Task.WhenAny
Ausdruck wartet nicht auf die fertige Aufgabe, sondern wartet auf das Task
von der Task.WhenAny
Methode zurückgegebene Objekt. Das Ergebnis der Task.WhenAny
Methode ist die abgeschlossene (oder fehlerhafte) Aufgabe. Die bewährte Methode besteht darin, erneut auf den Vorgang zu warten, auch wenn Sie wissen, dass die Aufgabe abgeschlossen ist. Auf diese Weise können Sie das Vorgangsergebnis abrufen oder sicherstellen, dass eine Ausnahme ausgelöst wird, die dazu führt, dass der Vorgang fehlerhaft ist.
Ü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 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();
}
}
}
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 Sprachfeatures für die async
und await
Schlüsselwörter bieten die Anleitung, die jeder ersichtlich aus den schriftlichen Anweisungen entnimmt: Beginnen Sie Aufgaben, wann immer es möglich ist, und blockieren Sie den Fortschritt nicht, während Sie darauf warten, dass Aufgaben abgeschlossen werden.