Udostępnij za pomocą


Przetwarzanie zadań asynchronicznych podczas ich wykonywania (C#)

Za pomocą programu Task.WhenAnymożna jednocześnie uruchomić wiele zadań i przetworzyć je jeden po drugim, zamiast przetwarzać je 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ńczą pobieranie. To zadanie jest usuwane z kolekcji i przetwarzane. Pętla powtarza się, dopóki kolekcja nie zawiera więcej zadań.

Wymagania wstępne

Aby wykonać czynności opisane w tym samouczku, użyj jednej z następujących opcji:

Tworzenie przykładowej aplikacji

Utwórz nową aplikację konsolową platformy .NET Core. Można go utworzyć przy użyciu polecenia dotnet new console lub programu Visual Studio.

Otwórz plik Program.cs w edytorze kodu i zastąp istniejący kod następującym 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 jest Main metoda . Zastąp istniejącą metodę następującym kodem:

static Task Main() => SumPageSizesAsync();

Zaktualizowana Main metoda jest teraz traktowana jako główna asynchroniczna, która umożliwia asynchroniczny punkt wejścia do pliku wykonywalnego. Jest wyrażona jako wywołanie metody SumPageSizesAsync.

Tworzenie metody asynchronicznej 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 kończy się pętla. Metoda rozpoczyna się od utworzenia wystąpienia i uruchomienia klasy Stopwatch. Następnie zawiera zapytanie, które po wykonaniu tworzy kolekcję zadań. Każde wywołanie metody ProcessUrlAsync w poniższym kodzie zwraca Task<TResult>wartość , 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łujesz 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. Awaits finishedTask, który jest zwracany przez wywołanie metody ProcessUrlAsync. Zmienna finishedTaskTask<TResult> to 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 odczytywania Task<TResult>.Result właściwości , która zgłasza AggregateExceptionbłąd .

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

W przypadku 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.

Ostrzeżenie

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.

Uproszczenie podejścia przy użyciu Task.WhenEach

Pętla while zaimplementowana w SumPageSizesAsync metodzie można uprościć przy użyciu nowej Task.WhenEach metody wprowadzonej na platformie .NET 9, wywołując ją w await foreach pętli.
Zastąp wcześniej zaimplementowaną while pętlę:

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

z uproszczonym await foreach:

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

To nowe podejście umożliwia nie wielokrotne wywoływanie Task.WhenAny ręcznego wywołania zadania i usunięcie tego, który zostanie zakończony, ponieważ Task.WhenEach iteruje zadanie w kolejności ich ukończenia.

Kompletny przykład

Poniższy kod jest kompletnym tekstem pliku Program.cs dla przykładu.

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 także