Przetwarzanie zadań asynchronicznych w miarę ich ukończenia (C#)

Za pomocą programu Task.WhenAnymożna jednocześnie uruchomić wiele zadań i przetworzyć je pojedynczo, ponieważ są one ukończone, a nie przetwarzać ich w kolejności, w której są uruchamiane.

W poniższym przykładzie użyto zapytania do utworzenia kolekcji zadań. Każde zadanie pobiera zawartość określonej witryny internetowej. W każdej iteracji pętli while oczekiwane wywołanie WhenAny zwraca zadanie w kolekcji zadań, które najpierw kończy pobieranie. To zadanie jest usuwane z kolekcji i przetwarzane. Pętla powtarza się, dopóki kolekcja nie zawiera więcej zadań.

Wymagania wstępne

Możesz wykonać czynności opisane w tym samouczku, korzystając z jednej z następujących opcji:

Tworzenie przykładowej aplikacji

Utwórz nową aplikację konsolową platformy .NET Core. Można je utworzyć za pomocą polecenia dotnet new console lub programu Visual Studio.

Otwórz plik Program.cs w edytorze kodu i zastąp istniejący kod tym kodem:

using System.Diagnostics;

namespace ProcessTasksAsTheyFinish;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

Dodawanie pól

Program W definicji klasy dodaj następujące dwa pola:

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

Funkcja HttpClient uwidacznia możliwość wysyłania żądań HTTP i odbierania odpowiedzi HTTP. Zawiera s_urlList wszystkie adresy URL, które aplikacja planuje przetworzyć.

Aktualizowanie punktu wejścia aplikacji

Głównym punktem wejścia do aplikacji konsolowej Main jest metoda. Zastąp istniejącą metodę następującymi elementami:

static Task Main() => SumPageSizesAsync();

Zaktualizowana Main metoda jest teraz uznawana za główny asynchroniczny, który umożliwia asynchroniczny punkt wejścia do pliku wykonywalnego. Jest wyrażona jako wywołanie metody SumPageSizesAsync.

Tworzenie asynchronicznej metody sumowania rozmiarów stron

Main Poniżej metody dodaj metodęSumPageSizesAsync:

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");
}

Pętla while usuwa jedno z zadań w każdej iteracji. Po zakończeniu każdego zadania pętla kończy się. Metoda rozpoczyna się od utworzenia wystąpienia i uruchomienia Stopwatchelementu . Następnie zawiera zapytanie, które po wykonaniu tworzy kolekcję zadań. Każde wywołanie metody ProcessUrlAsync w poniższym kodzie zwraca wartość Task<TResult>, gdzie TResult jest liczbą całkowitą:

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

Ze względu na odroczone wykonanie za pomocą LINQ wywołaj polecenie Enumerable.ToList , aby uruchomić każde zadanie.

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

Pętla while wykonuje następujące kroki dla każdego zadania w kolekcji:

  1. Oczekuje na wywołanie, aby WhenAny zidentyfikować pierwsze zadanie w kolekcji, które zakończyło pobieranie.

    Task<int> finishedTask = await Task.WhenAny(downloadTasks);
    
  2. Usuwa to zadanie z kolekcji.

    downloadTasks.Remove(finishedTask);
    
  3. Oczekuje na finishedTaskelement , który jest zwracany przez wywołanie metody ProcessUrlAsync. Zmienna finishedTask to Task<TResult> gdzie TResult jest liczbą całkowitą. Zadanie zostało już ukończone, ale oczekujesz na pobranie długości pobranej witryny internetowej, jak pokazano w poniższym przykładzie. Jeśli zadanie zostanie uszkodzone, await zgłosi pierwszy wyjątek podrzędny przechowywany w AggregateExceptionobiekcie , w przeciwieństwie do odczytu Task<TResult>.Result właściwości , co spowoduje zgłoszenie AggregateException.

    total += await finishedTask;
    

Dodawanie metody procesu

Dodaj następującą ProcessUrlAsync metodę SumPageSizesAsync poniżej metody:

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;
}

Dla dowolnego adresu URL metoda użyje podanego client wystąpienia, aby uzyskać odpowiedź jako byte[]. Długość jest zwracana po zapisaniu adresu URL i długości do konsoli.

Uruchom program kilka razy, aby sprawdzić, czy pobrane długości nie zawsze są wyświetlane w tej samej kolejności.

Przestroga

Możesz użyć WhenAny w pętli, zgodnie z opisem w przykładzie, aby rozwiązać problemy, które obejmują niewielką liczbę zadań. Jednak inne podejścia są bardziej wydajne, jeśli masz dużą liczbę zadań do przetworzenia. Aby uzyskać więcej informacji i przykładów, zobacz Przetwarzanie zadań podczas ich wykonywania.

Kompletny przykład

Poniższy kod jest kompletnym tekstem pliku Program.cs w tym przykładzie.

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

Zobacz też