Freigeben über


Verarbeiten von asynchronen Aufgaben nach deren Abschluss (C#)

Mithilfe von Task.WhenAny" können Sie mehrere Aufgaben gleichzeitig starten und sie einzeln verarbeiten, da sie abgeschlossen sind, anstatt sie in der Reihenfolge zu verarbeiten, in der sie gestartet werden.

Im folgenden Beispiel wird eine Abfrage verwendet, um eine Sammlung von Aufgaben zu erstellen. Jede Aufgabe lädt den Inhalt einer angegebenen Website herunter. Bei jeder Iteration einer While-Schleife gibt ein abgewarteter Aufruf an WhenAny die Aufgabenstellung in der Aufgabensammlung zurück, die den Download zuerst abgeschlossen hat. Diese Aufgabe wird aus der Sammlung entfernt und verarbeitet. Die Schleife wird wiederholt, bis die Auflistung keine weiteren Aufgaben enthält.

Voraussetzungen

Sie können diesem Lernprogramm folgen, indem Sie eine der folgenden Optionen verwenden:

Beispielanwendung erstellen

Erstellen Sie eine neue .NET Core-Konsolenanwendung. Sie können einen erstellen, indem Sie den dotnet new console Befehl oder Visual Studio verwenden.

Öffnen Sie die Program.cs Datei im Code-Editor, und ersetzen Sie den vorhandenen Code durch diesen 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://dori-uw-1.kuma-moon.com",
    "https://dori-uw-1.kuma-moon.com/aspnet/core",
    "https://dori-uw-1.kuma-moon.com/azure",
    "https://dori-uw-1.kuma-moon.com/azure/devops",
    "https://dori-uw-1.kuma-moon.com/dotnet",
    "https://dori-uw-1.kuma-moon.com/dynamics365",
    "https://dori-uw-1.kuma-moon.com/education",
    "https://dori-uw-1.kuma-moon.com/enterprise-mobility-security",
    "https://dori-uw-1.kuma-moon.com/gaming",
    "https://dori-uw-1.kuma-moon.com/graph",
    "https://dori-uw-1.kuma-moon.com/microsoft-365",
    "https://dori-uw-1.kuma-moon.com/office",
    "https://dori-uw-1.kuma-moon.com/powershell",
    "https://dori-uw-1.kuma-moon.com/sql",
    "https://dori-uw-1.kuma-moon.com/surface",
    "https://dori-uw-1.kuma-moon.com/system-center",
    "https://dori-uw-1.kuma-moon.com/visualstudio",
    "https://dori-uw-1.kuma-moon.com/windows",
    "https://dori-uw-1.kuma-moon.com/maui"
};

Dies HttpClient macht die Möglichkeit zum Senden von HTTP-Anforderungen und zum Empfangen von HTTP-Antworten verfügbar. 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 gilt nun als Async Main, das einen asynchronen Einstiegspunkt in der ausführbaren Datei ermöglicht. Sie wird als Aufruf zu SumPageSizesAsync ausgedrückt.

Asynchrone Methode zur Summierung von Seitengrößen erstellen

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. Nach Abschluss jeder Aufgabe endet die Schleife. Die Methode beginnt mit dem Instanziieren und Starten eines Stopwatch. Anschließend wird eine Abfrage eingeschlossen, die beim Ausführen eine Sammlung von Aufgaben erstellt. Jeder Aufruf von ProcessUrlAsync im folgenden Code gibt ein 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 in LINQ müssen Sie Enumerable.ToList aufrufen, um jede Aufgabe 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. Wartet auf einen Aufruf an WhenAny, um festzustellen, welche die erste Aufgabe in der Sammlung ist, die ihren Download abgeschlossen hat.

    Task<int> finishedTask = await Task.WhenAny(downloadTasks);
    
  2. Entfernt diese Aufgabe aus der Auflistung.

    downloadTasks.Remove(finishedTask);
    
  3. Wartet auf finishedTask, das durch einen Aufruf von ProcessUrlAsync zurückgegeben wird. Die finishedTask Variable ist ein Task<TResult>, wobei TResult eine ganze Zahl ist. Die Aufgabe ist bereits abgeschlossen, aber Sie warten darauf, die Länge der heruntergeladenen Website abzurufen, wie im folgenden Beispiel gezeigt. Wenn die Aufgabe fehlerhaft ist, führt await dazu, dass die erste untergeordnete Ausnahme, die in AggregateException gespeichert ist, ausgelöst wird, im Gegensatz zum Lesen der Task<TResult>.Result-Eigenschaft, die die AggregateException auslösen würde.

    total += await finishedTask;
    

Hinzufügen einer Prozessmethode

Fügen Sie die folgende ProcessUrlAsync Methode unter 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 jede angegebene URL nutzt die Methode die Instanz client, 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 sicherzustellen, dass die heruntergeladenen Längen nicht immer in derselben Reihenfolge angezeigt werden.

Vorsicht

Sie können WhenAny in einer Schleife verwenden, wie im Beispiel beschrieben, um Probleme zu lösen, die eine kleine Anzahl von Aufgaben umfassen. Andere Ansätze sind jedoch effizienter, wenn Sie über eine große Anzahl von Aufgaben verfügen, die verarbeitet werden sollen. Weitere Informationen und Beispiele finden Sie unter "Verarbeitungsaufgaben, wenn sie abgeschlossen sind".

Vereinfachen des Ansatzes mithilfe von Task.WhenEach

Die in der Methode SumPageSizesAsync implementierte while Schleife kann mithilfe der in .NET 9 eingeführten neuen Methode Task.WhenEach vereinfacht werden, indem diese in der await foreach Schleife aufgerufen wird.
Ersetzen Sie die zuvor implementierte while Schleife:

    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        total += await finishedTask;
    }

mit dem vereinfachten await foreach:

    await foreach (Task<int> t in Task.WhenEach(downloadTasks))
    {
        total += await t;
    }

Dieser neue Ansatz ermöglicht es, Task.WhenAny nicht mehr wiederholt manuell aufzurufen und die Aufgaben zu entfernen, die abgeschlossen sind, da Task.WhenEach die Aufgaben in der Reihenfolge ihrer Fertigstellung durchläuft.

Vollständiges Beispiel

Der folgende Code ist der vollständige 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://dori-uw-1.kuma-moon.com",
    "https://dori-uw-1.kuma-moon.com/aspnet/core",
    "https://dori-uw-1.kuma-moon.com/azure",
    "https://dori-uw-1.kuma-moon.com/azure/devops",
    "https://dori-uw-1.kuma-moon.com/dotnet",
    "https://dori-uw-1.kuma-moon.com/dynamics365",
    "https://dori-uw-1.kuma-moon.com/education",
    "https://dori-uw-1.kuma-moon.com/enterprise-mobility-security",
    "https://dori-uw-1.kuma-moon.com/gaming",
    "https://dori-uw-1.kuma-moon.com/graph",
    "https://dori-uw-1.kuma-moon.com/microsoft-365",
    "https://dori-uw-1.kuma-moon.com/office",
    "https://dori-uw-1.kuma-moon.com/powershell",
    "https://dori-uw-1.kuma-moon.com/sql",
    "https://dori-uw-1.kuma-moon.com/surface",
    "https://dori-uw-1.kuma-moon.com/system-center",
    "https://dori-uw-1.kuma-moon.com/visualstudio",
    "https://dori-uw-1.kuma-moon.com/windows",
    "https://dori-uw-1.kuma-moon.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://dori-uw-1.kuma-moon.com                                      132,517
// https://dori-uw-1.kuma-moon.com/powershell                            57,375
// https://dori-uw-1.kuma-moon.com/gaming                                33,549
// https://dori-uw-1.kuma-moon.com/aspnet/core                           88,714
// https://dori-uw-1.kuma-moon.com/surface                               39,840
// https://dori-uw-1.kuma-moon.com/enterprise-mobility-security          30,903
// https://dori-uw-1.kuma-moon.com/microsoft-365                         67,867
// https://dori-uw-1.kuma-moon.com/windows                               26,816
// https://dori-uw-1.kuma-moon.com/maui                               57,958
// https://dori-uw-1.kuma-moon.com/dotnet                                78,706
// https://dori-uw-1.kuma-moon.com/graph                                 48,277
// https://dori-uw-1.kuma-moon.com/dynamics365                           49,042
// https://dori-uw-1.kuma-moon.com/office                                67,867
// https://dori-uw-1.kuma-moon.com/system-center                         42,887
// https://dori-uw-1.kuma-moon.com/education                             38,636
// https://dori-uw-1.kuma-moon.com/azure                                421,663
// https://dori-uw-1.kuma-moon.com/visualstudio                          30,925
// https://dori-uw-1.kuma-moon.com/sql                                   54,608
// https://dori-uw-1.kuma-moon.com/azure/devops                          86,034

// Total bytes returned:    1,454,184
// Elapsed time:            00:00:01.1290403

Siehe auch