Beherrschung von C# Async und Await
Die asynchrone Programmierung kann mit ihrer komplexen Terminologie abschreckend wirken, aber sie bietet erhebliche Vorteile, indem sie Anwendungen schneller und reaktionsschneller macht. Das Video von Tim Corey zum Thema "C# Async / Await - Machen Sie Ihre Anwendung reaktionsschneller mit asynchroner Programmierung" bietet eine praktische Anleitung zur effektiven Nutzung dieser Funktionen.
In diesem Artikel verwenden wir das ausführliche Video von Tim Corey, um diese Konzepte aufzuschlüsseln und die praktischen Anwendungen von async und await anhand klarer Beispiele zu demonstrieren. Wir werden verschiedene Szenarien abdecken, von asynchronen Ereignishandlern bis hin zum Umgang mit mehreren Aufgaben, Synchronisationskontexten, Fehlerbehandlung und den üblichen Fallstricken wie der Verwendung von asynchronen ungültigen Methoden. Am Ende dieses Artikels werden Sie ein solides Verständnis der asynchronen Programmierung haben und wissen, wie Sie diese in Ihren eigenen C#-Anwendungen implementieren können.
Einführung
Asynchrone Programmierung in C# ist für die Verbesserung der Anwendungsleistung unerlässlich, insbesondere bei der Arbeit mit E/A- oder CPU-gebundenen Operationen. Die async und await Schlüsselwörter erlauben es Entwicklern, asynchronen Code zu schreiben, der langlaufende Aufgaben ausführen kann, ohne den Haupt-Thread zu blockieren. Indem Sie eine Methode mit dem Schlüsselwort async kennzeichnen, signalisieren Sie, dass die Methode eine asynchrone Operation durchführen wird und eine Task oder Task<t> zurückgibt. Das await Schlüsselwort wird innerhalb einer asynchronen Methode verwendet, um die Ausführung anzuhalten, bis die zu wartende Aufgabe abgeschlossen ist, sodass andere Operationen gleichzeitig ausgeführt werden können.
Im Gegensatz zur synchronen Programmierung, bei der Aufgaben sequentiell ausgeführt werden und den aufrufenden Thread blockieren können, ermöglichen asynchrone Methoden die gleichzeitige Bearbeitung mehrerer Aufgaben, z. B. Hintergrundprozesse oder die Verarbeitung von Benutzereingaben, ohne dass der UI-Thread eingefroren wird. In diesem Artikel werden wir erkunden, wie async und await zusammenarbeiten, indem wir Konzepte wie asynchrone void-Methoden, Fehlerbehandlung, Kontextwechsel und Synchronisierungskontexte abdecken. Sie lernen, wie Sie mehrere asynchrone Operationen effizient verwalten und Ausnahmen behandeln, um eine reibungslose Ausführung über den aufrufenden Thread, den UI-Thread und den Abschluss der Aufgabe sicherzustellen. Dieser Ansatz ist besonders nützlich für Szenarien mit Dateisystemen, Netzwerkanfragen oder gleichzeitigen Anfragen, bei denen es auf Reaktionsfähigkeit ankommt.
Tim führt in die asynchrone Programmierung ein, indem er den Unterschied zwischen synchronen und asynchronen Operationen erklärt. Bei der synchronen Programmierung werden Aufgaben sequentiell ausgeführt, was zu Verzögerungen führt, wenn eine Aufgabe lange dauert. Bei der asynchronen Programmierung können Aufgaben parallel oder im Hintergrund ausgeführt werden, was die Leistung und Reaktionsfähigkeit verbessert.
Vorteile der asynchronen Programmierung
Tim hebt zwei Hauptvorteile der asynchronen Programmierung hervor:
-
Verbesserte Reaktionsfähigkeit der Benutzeroberfläche: Durch die asynchrone Ausführung von Aufgaben bleibt die Benutzeroberfläche auch bei langwierigen Vorgängen reaktionsfähig.
- Parallele Ausführung: Unabhängige Aufgaben können parallel ausgeführt werden, wodurch sich die Gesamtzeit, die für ihre Ausführung benötigt wird, verringert.
Demo Anwendung Walk-through
Tim richtet eine Windows Presentation Foundation (WPF)-Demoanwendung ein, um den Unterschied zwischen synchronen und asynchronen Operationen zu demonstrieren. Die Anwendung verfügt über zwei Schaltflächen: eine für die synchrone Ausführung von Aufgaben und eine weitere für die asynchrone Ausführung von Aufgaben.
Demo-Anwendung
Die Anwendung enthält auch einen Ergebnisbereich, in dem die Ausgabe angezeigt wird.
Synchroner Betrieb
Tim erklärt den Code hinter der synchronen Operation. Ein Stopwatch wird verwendet, um die Ausführungszeit der Aufgabe zu messen.
private void ExecuteSync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
List<string> websites = GetWebsiteList();
foreach (var website in websites)
{
string result = DownloadWebsite(website);
ReportWebsiteInfo(website, result);
}
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
private List<string> GetWebsiteList()
{
return new List<string>
{
"https://www.yahoo.com",
"https://www.google.com",
"https://www.microsoft.com",
"https://www.cnn.com",
"https://www.codeproject.com",
"https://www.stackoverflow.com"
};
}
private string DownloadWebsite(string websiteURL)
{
WebClient client = new WebClient();
return client.DownloadString(websiteURL);
}
private void ReportWebsiteInfo(string website, string data)
{
ResultsWindow.Text += $"{website}: {data.Length} characters{Environment.NewLine}";
}
private void ExecuteSync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
List<string> websites = GetWebsiteList();
foreach (var website in websites)
{
string result = DownloadWebsite(website);
ReportWebsiteInfo(website, result);
}
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
private List<string> GetWebsiteList()
{
return new List<string>
{
"https://www.yahoo.com",
"https://www.google.com",
"https://www.microsoft.com",
"https://www.cnn.com",
"https://www.codeproject.com",
"https://www.stackoverflow.com"
};
}
private string DownloadWebsite(string websiteURL)
{
WebClient client = new WebClient();
return client.DownloadString(websiteURL);
}
private void ReportWebsiteInfo(string website, string data)
{
ResultsWindow.Text += $"{website}: {data.Length} characters{Environment.NewLine}";
}
Dieser Code führt die folgenden Schritte aus:
- Start der Stoppuhr: Misst die Ausführungszeit.
- Die Website-Liste abrufen: Ruft eine Liste von Website-URLs ab.
- Jede Website herunterladen: Lädt den Inhalt der einzelnen Websites herunter.
- Informationen zur Website melden: Zeigt die URL und die Länge des heruntergeladenen Inhalts an.
- Stop the Stopwatch: Hält den Timer an und meldet die Gesamtausführungszeit.
Überwachung der synchronen Operation
Tim führt die Anwendung aus, um den synchronen Betrieb zu demonstrieren. Er stellt fest, dass die Benutzeroberfläche nicht mehr reagiert, während die Websites heruntergeladen werden, und dass die Ergebnisse auf einmal angezeigt werden, nachdem die Downloads abgeschlossen sind.
Synchroner Vorgang
Für eine praktische Demonstration des Formulars, das sich nicht bewegt, sehen Sie sich das Video zum besseren Verständnis bei 9:20 an.
Erstellen einer asynchronen Aufgabe
Tim geht das erste Problem an, indem er die synchrone Methode in eine asynchrone Methode umwandelt. Dies beinhaltet die Verwendung der Schlüsselwörter async und await. Hier ist eine schrittweise Erklärung des Prozesses:
-
Kopieren einer vorhandenen synchronen Methode:
- Tim kopiert die vorhandene synchrone Methode und benennt sie um, um anzugeben, dass sie asynchron sein wird.
private async Task RunDownloadAsync() { // Same code as RunDownloadSync, but will be modified for async }private async Task RunDownloadAsync() { // Same code as RunDownloadSync, but will be modified for async } -
Download-Aufruf für asynchrone Ausführung modifizieren:
- Tim bettet den Download-Aufruf in ein
Task.Runein, um ihn asynchron auszuführen.
private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { return await Task.Run(() => DownloadWebsite(websiteURL)); }private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { return await Task.Run(() => DownloadWebsite(websiteURL)); }- Das
awaitSchlüsselwort stellt sicher, dass die Methode wartet, bis die asynchrone Aufgabe abgeschlossen ist, bevor sie fortfährt.
- Tim bettet den Download-Aufruf in ein
-
Sicherstellen, dass die Methode asynchron ist:
- Die Methodensignatur wird aktualisiert, um das
asyncSchlüsselwort einzuschließen, und sie gibt eine Task zurück.
private async Task RunDownloadAsync() { List<string> websites = GetWebsiteList(); foreach (var website in websites) { var result = await DownloadWebsiteAsync(website); ReportWebsiteInfo(website, result); } }private async Task RunDownloadAsync() { List<string> websites = GetWebsiteList(); foreach (var website in websites) { var result = await DownloadWebsiteAsync(website); ReportWebsiteInfo(website, result); } } - Die Methodensignatur wird aktualisiert, um das
-
Richtig mit dem Ereignis umgehen:
- Der Ereignis-Handler für das Klicken einer Schaltfläche wird aktualisiert, um die neue asynchrone Methode aufzurufen.
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e) { Stopwatch stopwatch = Stopwatch.StartNew(); await RunDownloadAsync(); stopwatch.Stop(); ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}"; }private async void ExecuteAsync_Click(object sender, RoutedEventArgs e) { Stopwatch stopwatch = Stopwatch.StartNew(); await RunDownloadAsync(); stopwatch.Stop(); ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}"; }- Beachten Sie, dass Ereignis-Handler einen ungültigen Wert zurückgeben können, auch wenn sie asynchrone Methoden aufrufen.
Die Reaktionsfähigkeit der Benutzeroberfläche verbessern
Tim zeigt, dass durch die Verwendung des await Schlüsselwortes die Benutzeroberfläche reaktionsfähig bleibt. Dies erlaubt es Benutzern, mit dem Fenster zu interagieren, während die asynchrone Aufgabe läuft.
// UI remains responsive
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
// UI remains responsive
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
Korrektes Timing sicherstellen
Um sicherzustellen, dass die gesamte Ausführungszeit korrekt gemeldet wird, fügt Tim das await Schlüsselwort zum Aufruf im Button-Klick-Ereignishandler hinzu.
// Correctly waits for the asynchronous task to complete
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
// Correctly waits for the asynchronous task to complete
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
Durch das Warten auf den Abschluss der asynchronen Methode wird die Ausführungszeit genau gemessen, und die Ergebnisse werden angezeigt, sobald sie abgerufen werden.
Parallele Asynchronität erstellen
Tim geht auf die Einschränkung ein, dass man auf die sequentielle Ausführung jeder Aufgabe warten muss, indem er die parallele Ausführung verwendet. So ändert er den Code, um dies zu erreichen:
-
Kopieren der vorhandenen Async-Methode:
- Tim dupliziert die vorhandene async-Methode und benennt sie um, um auf die parallele Ausführung hinzuweisen.
private async Task RunDownloadParallelAsync() { // Parallel execution logic will be added here }private async Task RunDownloadParallelAsync() { // Parallel execution logic will be added here } -
Erstellen Sie eine Liste von Aufgaben:
- Es wird eine Liste mit Aufgaben erstellt, in der alle Download-Aufgaben gespeichert werden.
List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>();List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>(); -
Alle Aufgaben ohne Wartezeit einleiten:
- Anstatt jede Download-Aufgabe sofort abzuwarten, fügt Tim sie der Liste der Aufgaben hinzu.
foreach (var website in websites) { tasks.Add(DownloadWebsiteAsync(website)); }foreach (var website in websites) { tasks.Add(DownloadWebsiteAsync(website)); } -
Alle Aufgaben abwarten:
- Die
Task.WhenAllMethode wird verwendet, um das Abschließen aller Aufgaben zu erwarten. Diese Methode gibt ein Array von Ergebnissen zurück, sobald alle Aufgaben abgeschlossen sind.
WebsiteDataModel[] results = await Task.WhenAll(tasks);WebsiteDataModel[] results = await Task.WhenAll(tasks); - Die
-
Bearbeiten der Ergebnisse:
- Nachdem alle Aufgaben abgeschlossen sind, verarbeitet Tim die Ergebnisse in einer Schleife.
foreach (var result in results) { ReportWebsiteInfo(result); }foreach (var result in results) { ReportWebsiteInfo(result); }
Hier ist der vollständige Code für die parallele Ausführungsmethode:
private async Task RunDownloadParallelAsync()
{
List<string> websites = GetWebsiteList();
List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>();
foreach (var website in websites)
{
tasks.Add(DownloadWebsiteAsync(website));
}
WebsiteDataModel[] results = await Task.WhenAll(tasks);
foreach (var result in results)
{
ReportWebsiteInfo(result);
}
}
private async Task RunDownloadParallelAsync()
{
List<string> websites = GetWebsiteList();
List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>();
foreach (var website in websites)
{
tasks.Add(DownloadWebsiteAsync(website));
}
WebsiteDataModel[] results = await Task.WhenAll(tasks);
foreach (var result in results)
{
ReportWebsiteInfo(result);
}
}
Aktualisierung des Event-Handlers
Tim aktualisiert den Ereignis-Handler für den Schaltflächenklick, um die neue parallele Ausführungsmethode aufzurufen.
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadParallelAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadParallelAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
Durch das Warten auf RunDownloadParallelAsync wird die gesamte Ausführungszeit genau gemessen und gemeldet.
Beobachtung der Leistungsverbesserung
Tim demonstriert die Leistungsverbesserung, indem er die Anwendung mit paralleler Ausführung ausführt. Die Ergebnisse zeigen eine erhebliche Reduzierung der Gesamtausführungszeit im Vergleich zur sequenziellen Ausführung.
// Parallel execution
async Task RunDownloadParallelAsync();
// Parallel execution
async Task RunDownloadParallelAsync();
Der Geschwindigkeitszuwachs ist offensichtlich, da die Websites gleichzeitig heruntergeladen werden, wodurch die Gesamtwartezeit auf die Dauer des langsamsten einzelnen Downloads reduziert wird.
Methoden in Task.Run() einbetten vs. asynchroner Methodenaufruf
Tim erklärt das Konzept der Verwendung von Task.Run(), um eine synchrone Methode einzubetten und sie asynchron zu machen, wenn Sie die Originalmethode nicht ändern können. Er zeigt aber auch den bevorzugten Ansatz, die Methode selbst so zu modifizieren, dass sie asynchron ist, wenn man die Kontrolle darüber hat.
-
Methode in
Task.Run()einbetten:- Dieser Ansatz ist nützlich, wenn Sie den Code der Originalmethode nicht ändern können, sie aber dennoch asynchron ausführen möchten.
private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { return await Task.Run(() => DownloadWebsite(websiteURL)); }private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { return await Task.Run(() => DownloadWebsite(websiteURL)); } -
Eine Methode asynchron machen:
-
Wenn Sie die Methode ändern können, ist es besser, die Methode selbst zu konvertieren, um asynchron zu sein, indem Sie eine geeignete asynchrone API verwenden.
- Tim demonstriert dies, indem er
DownloadWebsitezuDownloadWebsiteAsyncändert undDownloadStringTaskAsyncausWebClientverwendet.
private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { WebClient client = new WebClient(); string data = await client.DownloadStringTaskAsync(websiteURL); return new WebsiteDataModel { URL = websiteURL, Data = data }; }private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { WebClient client = new WebClient(); string data = await client.DownloadStringTaskAsync(websiteURL); return new WebsiteDataModel { URL = websiteURL, Data = data }; } -
-
Anpassung der Aufrufmethode:
- Nach der Umwandlung der Methode muss der aufrufende Code angepasst werden, um den
Task.Run()Wrapper zu entfernen und die asynchrone Methode direkt aufzurufen.
private async Task RunDownloadParallelAsync() { List<string> websites = GetWebsiteList(); List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>(); foreach (var website in websites) { tasks.Add(DownloadWebsiteAsync(website)); } WebsiteDataModel[] results = await Task.WhenAll(tasks); foreach (var result in results) { ReportWebsiteInfo(result); } }private async Task RunDownloadParallelAsync() { List<string> websites = GetWebsiteList(); List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>(); foreach (var website in websites) { tasks.Add(DownloadWebsiteAsync(website)); } WebsiteDataModel[] results = await Task.WhenAll(tasks); foreach (var result in results) { ReportWebsiteInfo(result); } } - Nach der Umwandlung der Methode muss der aufrufende Code angepasst werden, um den
Zusammenfassung der wichtigsten Punkte
Abschließend gibt Tim noch einige wichtige Hinweise zur Verwendung von async und await:
-
Sicherstellen, dass Methoden Aufgaben zurückgeben: Wann immer Sie eine Methode als
asynckennzeichnen, sollte sie eineTaskoderTask<t>anstelle von void zurückgeben (außer für Ereignishandler). -
Verwenden Sie
awaitfür zuverlässige Operationen: Verwenden Sie dasawaitSchlüsselwort, wenn Sie das Ergebnis einer asynchronen Operation benötigen, bevor Sie fortfahren. -
Aufgaben-Wrapper für nicht änderbaren Code: Verwenden Sie
Task.Run(), um synchrone Methoden einzubetten, wenn Sie die Originalmethode nicht ändern können. -
Asynchrone Methoden angemessen kennzeichnen: Fügen Sie immer
Asynczum Methodennamen hinzu, um anzuzeigen, dass es sich um eine asynchrone Operation handelt. - Parallele vs. Sequentielle Ausführung: Entscheiden Sie anhand der Abhängigkeiten zwischen den Aufgaben, ob diese parallel ausgeführt werden können oder aufeinander warten sollen.
Tim betont, dass die asynchrone Programmierung in C# durch async, await und Task vereinfacht wurde. Komplexe Themen wie Threading-Kontexte und Apartment-Modelle werden im Hintergrund behandelt.
Abschluss
Tim Coreys Tutorial zu async und await in C# bietet eine unschätzbare Ressource für die Beherrschung der asynchronen Programmierung. Durch die sorgfältige Erläuterung von Konzepten wie Task-Parallelität, Reaktionsfähigkeit der Benutzeroberfläche und die Bedeutung der Verwendung von async und await im richtigen Kontext gibt Tim den Entwicklern die Werkzeuge an die Hand, um schnellere und reaktionsfähigere Anwendungen zu erstellen. Sein detailliertes Walkthrough zeigt, wie man effizient mit lang laufenden Aufgaben umgeht, ohne den Haupt-Thread zu blockieren, sowie Strategien zur Verwaltung asynchroner Operationen und paralleler Ausführung.
Für ein tieferes Verständnis und zusätzliche praktische Beispiele empfehle ich das Video von Tim Corey. Seine Einblicke in async void-Methoden, Fehlerbehandlung und die Verwaltung von Synchronisationskontexten werden Ihre Fähigkeit zur Implementierung dieser Konzepte in Ihren Projekten weiter verbessern.
