Verarbeiten asynchroner Aufgaben nach Abschluss (C#)
Mit Task.WhenAny können Sie mehrere Aufgaben gleichzeitig starten und diese nicht in der Reihenfolge, in der sie gestartet wurden, sondern zu dem Zeitpunkt, zu dem sie abgeschlossen werden, verarbeiten.
Im folgenden Beispiel wird eine Abfrage verwendet, um eine Auflistung von Aufgaben zu erstellen. Jede Aufgabe lädt den Inhalt einer angegebenen Website herunter. In jeder Iteration einer While-Schleife gibt ein erwarteter Aufruf von WhenAny die Aufgabe in der Auflistung von Aufgaben zurück, die ihren Download zuerst beendet. Diese Aufgabe wird aus der Auflistung entfernt und verarbeitet. Die Ausführung der Schleife wird wiederholt, bis die Auflistung keine Aufgaben mehr enthält.
Voraussetzungen
Sie können die Schritte dieses Tutorials mithilfe einer der folgenden Optionen befolgen:
- Visual Studio 2022 mit installierter Workload .NET-Desktopentwicklung. Das .NET SDK wird automatisch installiert, wenn Sie diese Workload auswählen.
- Das .NET 5.0 SDK mit einem Code-Editor Ihrer Wahl, z. B. Visual Studio Code
Erstellen einer Beispielanwendung
Erstellen Sie eine neue .NET Core-Konsolenanwendung. Sie können mithilfe des dotnet new console-Befehls oder über Visual Studio eine solche Anwendung erstellen.
Öffnen Sie die Datei Program.cs in Ihrem Code-Editor, und ersetzen Sie den vorhandenen Code durch folgenden Code:
using System.Diagnostics;
namespace ProcessTasksAsTheyFinish;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
Hinzufügen von Feldern
Fügen Sie in der Program
-Klassendefinition die folgenden beiden Felder hinzu:
static readonly HttpClient s_client = new HttpClient
{
MaxResponseContentBufferSize = 1_000_000
};
static readonly IEnumerable<string> s_urlList = new string[]
{
"https://learn.microsoft.com",
"https://learn.microsoft.com/aspnet/core",
"https://learn.microsoft.com/azure",
"https://learn.microsoft.com/azure/devops",
"https://learn.microsoft.com/dotnet",
"https://learn.microsoft.com/dynamics365",
"https://learn.microsoft.com/education",
"https://learn.microsoft.com/enterprise-mobility-security",
"https://learn.microsoft.com/gaming",
"https://learn.microsoft.com/graph",
"https://learn.microsoft.com/microsoft-365",
"https://learn.microsoft.com/office",
"https://learn.microsoft.com/powershell",
"https://learn.microsoft.com/sql",
"https://learn.microsoft.com/surface",
"https://learn.microsoft.com/system-center",
"https://learn.microsoft.com/visualstudio",
"https://learn.microsoft.com/windows",
"https://learn.microsoft.com/maui"
};
Der HttpClient
ermöglicht das Senden von HTTP-Anforderungen und das Empfangen von HTTP-Antworten. s_urlList
enthält alle URLs, die von der Anwendung verarbeitet werden sollen.
Aktualisieren des Einstiegspunkts der Anwendung
Der Haupteinstiegspunkt in die Konsolenanwendung ist die Main
-Methode. Ersetzen Sie die vorhandene Methode durch Folgendes:
static Task Main() => SumPageSizesAsync();
Die aktualisierte Main
-Methode wird jetzt als Async main-Methode betrachtet, die einen asynchronen Einstiegspunkt in die ausführbare Datei ermöglicht. Sie wird mit einem Aufruf von SumPageSizesAsync
angegeben.
Erstellen der SumPageSizesAsync-Methode
Fügen Sie unter der Main
-Methode die SumPageSizesAsync
-Methode hinzu:
static async Task SumPageSizesAsync()
{
var stopwatch = Stopwatch.StartNew();
IEnumerable<Task<int>> downloadTasksQuery =
from url in s_urlList
select ProcessUrlAsync(url, s_client);
List<Task<int>> downloadTasks = downloadTasksQuery.ToList();
int total = 0;
while (downloadTasks.Any())
{
Task<int> finishedTask = await Task.WhenAny(downloadTasks);
downloadTasks.Remove(finishedTask);
total += await finishedTask;
}
stopwatch.Stop();
Console.WriteLine($"\nTotal bytes returned: {total:#,#}");
Console.WriteLine($"Elapsed time: {stopwatch.Elapsed}\n");
}
Die while
-Schleife entfernt eine der Aufgaben in jeder Iteration. Die Schleife endet nach Abschluss aller Aufgaben. Die Methode beginnt mit dem Instanziieren und Starten einer Stopwatch-Klasse. Dann ist eine Abfrage enthalten, die eine Auflistung von Aufgaben erstellt, wenn sie ausgeführt wird. Jeder Aufruf von ProcessUrlAsync
im folgenden Code gibt Task<TResult> zurück, wobei TResult
eine ganze Zahl ist:
IEnumerable<Task<int>> downloadTasksQuery =
from url in s_urlList
select ProcessUrlAsync(url, s_client);
Aufgrund der verzögerten Ausführung mit LINQ wird Enumerable.ToList aufgerufen, um die einzelnen Aufgaben zu starten.
List<Task<int>> downloadTasks = downloadTasksQuery.ToList();
Die while
-Schleife führt die folgenden Schritte für jede Aufgabe in der Auflistung aus:
Es wird ein Aufruf von
WhenAny
erwartet, um die erste Aufgabe in der Auflistung zu identifizieren, die ihren Download beendet hat.Task<int> finishedTask = await Task.WhenAny(downloadTasks);
Entfernt die entsprechende Aufgabe aus der Auflistung.
downloadTasks.Remove(finishedTask);
Erwartet
finishedTask
, das durch einen Aufruf vonProcessUrlAsync
zurückgegeben wird. Die VariablefinishedTask
ist eine Task<TResult>, wobeiTResult
eine ganze Zahl ist. Die Aufgabe ist bereits abgeschlossen, aber es darauf gewartet, dass von ihr die Länge der heruntergeladenen Website abgerufen wird, wie im folgenden Beispiel dargestellt. Wenn für die Aufgabe ein Fehler auftritt, löstawait
im Gegensatz zum Lesen der Task<TResult>.Result-Eigenschaft, die dieAggregateException
auslösen würde, die erste untergeordnete Ausnahme aus, die in derAggregateException
gespeichert ist.total += await finishedTask;
Hinzufügen der Prozessmethode
Fügen Sie die folgende ProcessUrlAsync
-Methode unterhalb der SumPageSizesAsync
-Methode hinzu:
static async Task<int> ProcessUrlAsync(string url, HttpClient client)
{
byte[] content = await client.GetByteArrayAsync(url);
Console.WriteLine($"{url,-60} {content.Length,10:#,#}");
return content.Length;
}
Für eine beliebige URL verwendet die Methode die client
-Instanz, die bereitgestellt wird, um die Antwort als byte[]
zu erhalten. Die Länge wird zurückgegeben, nachdem die URL und die Länge in die Konsole geschrieben wurden.
Führen Sie das Programm mehrmals aus, um zu bestätigen, dass die heruntergeladenen Längen nicht immer in der gleichen Reihenfolge angezeigt werden.
Achtung
Sie können WhenAny
in einer Schleife gemäß der Beschreibung im Beispiel verwenden, um Problemlösungen zu entwickeln, die nur wenige Aufgaben umfassen. Andere Ansätze sind jedoch effizienter, wenn viele Aufgaben verarbeitet werden müssen. Weitere Informationen und Beispiele finden Sie unter Processing Tasks as they complete (Verarbeitung von Aufgaben nach deren Abschluss).
Vollständiges Beispiel
Der folgende Code besteht aus dem vollständigen Text der Program.cs-Datei für das Beispiel.
using System.Diagnostics;
HttpClient s_client = new()
{
MaxResponseContentBufferSize = 1_000_000
};
IEnumerable<string> s_urlList = new string[]
{
"https://learn.microsoft.com",
"https://learn.microsoft.com/aspnet/core",
"https://learn.microsoft.com/azure",
"https://learn.microsoft.com/azure/devops",
"https://learn.microsoft.com/dotnet",
"https://learn.microsoft.com/dynamics365",
"https://learn.microsoft.com/education",
"https://learn.microsoft.com/enterprise-mobility-security",
"https://learn.microsoft.com/gaming",
"https://learn.microsoft.com/graph",
"https://learn.microsoft.com/microsoft-365",
"https://learn.microsoft.com/office",
"https://learn.microsoft.com/powershell",
"https://learn.microsoft.com/sql",
"https://learn.microsoft.com/surface",
"https://learn.microsoft.com/system-center",
"https://learn.microsoft.com/visualstudio",
"https://learn.microsoft.com/windows",
"https://learn.microsoft.com/maui"
};
await SumPageSizesAsync();
async Task SumPageSizesAsync()
{
var stopwatch = Stopwatch.StartNew();
IEnumerable<Task<int>> downloadTasksQuery =
from url in s_urlList
select ProcessUrlAsync(url, s_client);
List<Task<int>> downloadTasks = downloadTasksQuery.ToList();
int total = 0;
while (downloadTasks.Any())
{
Task<int> finishedTask = await Task.WhenAny(downloadTasks);
downloadTasks.Remove(finishedTask);
total += await finishedTask;
}
stopwatch.Stop();
Console.WriteLine($"\nTotal bytes returned: {total:#,#}");
Console.WriteLine($"Elapsed time: {stopwatch.Elapsed}\n");
}
static async Task<int> ProcessUrlAsync(string url, HttpClient client)
{
byte[] content = await client.GetByteArrayAsync(url);
Console.WriteLine($"{url,-60} {content.Length,10:#,#}");
return content.Length;
}
// Example output:
// https://learn.microsoft.com 132,517
// https://learn.microsoft.com/powershell 57,375
// https://learn.microsoft.com/gaming 33,549
// https://learn.microsoft.com/aspnet/core 88,714
// https://learn.microsoft.com/surface 39,840
// https://learn.microsoft.com/enterprise-mobility-security 30,903
// https://learn.microsoft.com/microsoft-365 67,867
// https://learn.microsoft.com/windows 26,816
// https://learn.microsoft.com/maui 57,958
// https://learn.microsoft.com/dotnet 78,706
// https://learn.microsoft.com/graph 48,277
// https://learn.microsoft.com/dynamics365 49,042
// https://learn.microsoft.com/office 67,867
// https://learn.microsoft.com/system-center 42,887
// https://learn.microsoft.com/education 38,636
// https://learn.microsoft.com/azure 421,663
// https://learn.microsoft.com/visualstudio 30,925
// https://learn.microsoft.com/sql 54,608
// https://learn.microsoft.com/azure/devops 86,034
// Total bytes returned: 1,454,184
// Elapsed time: 00:00:01.1290403