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:

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/xamarin"
};

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:

  1. 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);
    
  2. Entfernt die entsprechende Aufgabe aus der Auflistung.

    downloadTasks.Remove(finishedTask);
    
  3. Erwartet finishedTask, das durch einen Aufruf von ProcessUrlAsync zurückgegeben wird. Die Variable finishedTask ist eine Task<TResult>, wobei TResult 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öst await im Gegensatz zum Lesen der Task<TResult>.Result-Eigenschaft, die die AggregateException auslösen würde, die erste untergeordnete Ausnahme aus, die in der AggregateException 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/xamarin"
};

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/xamarin                               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

Weitere Informationen