Freigeben über


Verarbeiten asynchroner Aufgaben während der Ausführung (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 erwarteter Aufruf WhenAny die Aufgabe in der Sammlung von Aufgaben zurück, die den Download zuerst abgeschlossen haben. 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 neuen Konsolenbefehl dotnet 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://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"
};

Dies HttpClient macht die Möglichkeit zum Senden von HTTP-Anforderungen und zum Empfangen von HTTP-Antworten verfügbar. Enthält s_urlList 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 Asynchroner Haupt, der einen asynchronen Einstiegspunkt in die ausführbare Datei ermöglicht. Sie wird als Aufruf ausgedrückt SumPageSizesAsync.

Erstellen der Methode für asynchrone Seitengrößen für die Summe

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 ProcessUrlAsync im folgenden Code gibt eine Task<TResult>ganze Zahl zurück, wobei TResult es sich um eine ganze Zahl handelt:

IEnumerable<Task<int>> downloadTasksQuery =
    from url in s_urlList
    select ProcessUrlAsync(url, s_client);

Aufgrund der verzögerten Ausführung mit dem LINQ rufen Enumerable.ToList Sie auf, um jede Aufgabe zu starten.

List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

Die while Schleife führt die folgenden Schritte für jeden Vorgang in der Auflistung aus:

  1. Wartet auf einen Aufruf, um die erste Aufgabe in der Sammlung zu WhenAny identifizieren, die den Download abgeschlossen hat.

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

    downloadTasks.Remove(finishedTask);
    
  3. Awaits finishedTask, which is returned by a call to ProcessUrlAsync. Die finishedTask Variable ist eine Task<TResult> Stelle, an TResult der es sich um eine ganze Zahl handelt. 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, löst sie die erste untergeordnete Ausnahme aus, await die im Gegensatz zum Lesen der Task<TResult>.Result Eigenschaft gespeichert ist, wodurch die AggregateExceptionAggregateException.

    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 verwendet die Methode die client bereitgestellte Instanz, um die Antwort als eine byte[]. 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 in einer Schleife verwenden WhenAny , 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 während der Ausführung".

Vereinfachen des Ansatzes mithilfe von Task.WhenEach

Die while in SumPageSizesAsync der Methode implementierte Schleife kann mit der in .NET 9 eingeführten neuen Task.WhenEach Methode vereinfacht werden, indem sie sie in await foreach Schleife aufruft.
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;
    }

Mit diesem neuen Ansatz kann nicht mehr wiederholt aufgerufen werden, um einen Vorgang manuell aufzurufen Task.WhenAny und den Vorgang zu entfernen, der abgeschlossen ist, da Task.WhenEach der Vorgang in einer Reihenfolge seiner Fertigstellung durchlaufen wird.

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

Siehe auch