Partager via


Traiter des tâches asynchrones au fur et à mesure qu’elles se terminent (C#)

En utilisant Task.WhenAny, vous pouvez démarrer plusieurs tâches en même temps et les traiter une par une, au lieu de les traiter dans l’ordre dans lequel elles sont démarrées.

L’exemple suivant utilise une requête pour créer une collection de tâches. Chaque tâche télécharge le contenu d’un site web spécifié. Dans chaque itération d’une boucle while, un appel attendu pour WhenAny renvoyer la tâche dans la collection de tâches qui termine son téléchargement en premier. Cette tâche est supprimée de la collection et traitée. La boucle se répète jusqu’à ce que la collection ne contienne plus de tâches.

Prerequisites

Vous pouvez suivre ce tutoriel à l’aide de l’une des options suivantes :

Créer un exemple d’application

Créez une application console .NET Core. Vous pouvez en créer un à l’aide de la commande dotnet new console ou à partir de Visual Studio.

Ouvrez le fichier Program.cs dans votre éditeur de code et remplacez le code existant par ce code :

using System.Diagnostics;

namespace ProcessTasksAsTheyFinish;

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

Ajouter des champs

Dans la définition de Program classe, ajoutez les deux champs suivants :

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

Il HttpClient expose la possibilité d’envoyer des requêtes HTTP et de recevoir des réponses HTTP. Contient s_urlList toutes les URL que l’application prévoit de traiter.

Mettre à jour le point d’entrée de l’application

Le point d’entrée principal dans l’application console est la Main méthode. Remplacez la méthode existante par ce qui suit :

static Task Main() => SumPageSizesAsync();

La méthode mise à jour Main est désormais considérée comme un principal asynchrone, ce qui permet un point d’entrée asynchrone dans l’exécutable. Elle est exprimée sous la forme d’un appel à SumPageSizesAsync.

Créer la méthode de taille de page de somme asynchrone

Sous la Main méthode, ajoutez la SumPageSizesAsync méthode :

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

La while boucle supprime l’une des tâches de chaque itération. Une fois chaque tâche terminée, la boucle se termine. La méthode commence par instancier et démarrer un Stopwatch. Il inclut ensuite une requête qui, lorsqu’elle est exécutée, crée une collection de tâches. Chaque appel au ProcessUrlAsync code suivant retourne un Task<TResult>entier :TResult

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

En raison d’une exécution différée avec LINQ, vous appelez Enumerable.ToList pour démarrer chaque tâche.

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

La while boucle effectue les étapes suivantes pour chaque tâche de la collection :

  1. Attend un appel pour WhenAny identifier la première tâche de la collection qui a terminé son téléchargement.

    Task<int> finishedTask = await Task.WhenAny(downloadTasks);
    
  2. Supprime cette tâche de la collection.

    downloadTasks.Remove(finishedTask);
    
  3. Awaits finishedTask, qui est retourné par un appel à ProcessUrlAsync. La finishedTask variable est un Task<TResult>TResult entier. La tâche est déjà terminée, mais vous l’attendez pour récupérer la longueur du site web téléchargé, comme l’illustre l’exemple suivant. Si la tâche est défaillante, await lève la première exception enfant stockée dans le AggregateException, contrairement à la lecture de la Task<TResult>.Result propriété, qui lève le AggregateException.

    total += await finishedTask;
    

Ajouter une méthode de processus

Ajoutez la méthode suivante ProcessUrlAsync sous la SumPageSizesAsync méthode :

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

Pour toute URL donnée, la méthode utilise l’instance client fournie pour obtenir la réponse en tant que byte[]. La longueur est retournée après l’URL et la longueur est écrite dans la console.

Exécutez le programme plusieurs fois pour vérifier que les longueurs téléchargées n’apparaissent pas toujours dans le même ordre.

Caution

Vous pouvez utiliser WhenAny dans une boucle, comme décrit dans l’exemple, pour résoudre les problèmes impliquant un petit nombre de tâches. Toutefois, d’autres approches sont plus efficaces si vous avez un grand nombre de tâches à traiter. Pour plus d’informations et d’exemples, consultez Les tâches de traitement à mesure qu’elles se terminent.

Simplifier l’approche à l’aide de Task.WhenEach

La while boucle implémentée dans SumPageSizesAsync la méthode peut être simplifiée à l’aide de la nouvelle Task.WhenEach méthode introduite dans .NET 9, en l’appelant en await foreach boucle.
Remplacez la boucle précédemment implémentée while :

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

avec les opérations simplifiées await foreach:

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

Cette nouvelle approche permet de ne plus appeler Task.WhenAny manuellement une tâche et de supprimer celle qui se termine, car Task.WhenEach effectue une itération dans une tâche dans un ordre de leur achèvement.

Exemple complet

Le code suivant est le texte complet du fichier Program.cs pour l’exemple.

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

Voir aussi