140 votes

Comment limiter le nombre d'opérations d'E/S asynchrones simultanées ?

// let's say there is a list of 1000+ URLs
string[] urls = { "http://google.com", "http://yahoo.com", ... };

// now let's send HTTP requests to each of these URLs in parallel
urls.AsParallel().ForAll(async (url) => {
    var client = new HttpClient();
    var html = await client.GetStringAsync(url);
});

Voici le problème, il lance plus de 1000 requêtes web simultanées. Existe-t-il un moyen simple de limiter le nombre de ces requêtes http asynchrones ? De sorte que pas plus de 20 pages web soient téléchargées à un moment donné. Comment le faire de la manière la plus efficace possible ?

3voto

Theodor Zoulias Points 1088

Après la sortie de la .NET 6 (en novembre 2021), le moyen recommandé pour limiter le nombre d'opérations d'E/S asynchrones simultanées est la fonction Parallel.ForEachAsync avec l'API MaxDegreeOfParallelism configuration. Voici comment l'utiliser en pratique :

// let's say there is a list of 1000+ URLs
string[] urls = { "http://google.com", "http://yahoo.com", /*...*/ };
var client = new HttpClient();
var options = new ParallelOptions() { MaxDegreeOfParallelism = 20 };

// now let's send HTTP requests to each of these URLs in parallel
await Parallel.ForEachAsync(urls, options, async (url, cancellationToken) =>
{
    var html = await client.GetStringAsync(url, cancellationToken);
});

Dans l'exemple ci-dessus, le Parallel.ForEachAsync est attendue de manière asynchrone. Vous pouvez également Wait de manière synchrone si nécessaire, ce qui bloquera le thread actuel jusqu'à la fin de toutes les opérations asynchrones. Le processus synchrone Wait a l'avantage qu'en cas d'erreurs, toutes les exceptions seront propagées. Au contraire, le await ne propage, par conception, que la première exception. Si cela pose un problème, vous pouvez trouver des solutions aquí .

(Remarque : une mise en œuvre idiomatique d'une <code>ForEachAsync</code> qui propage également les résultats, peut être trouvée dans le fichier <a href="https://stackoverflow.com/revisions/64455549/4">4ème révision </a>de cette réponse)

2voto

vitidev Points 116

L'exemple de Theo Yaung est bien, mais il existe une variante sans liste de tâches en attente.

 class SomeChecker
 {
    private const int ThreadCount=20;
    private CountdownEvent _countdownEvent;
    private SemaphoreSlim _throttler;

    public Task Check(IList<string> urls)
    {
        _countdownEvent = new CountdownEvent(urls.Count);
        _throttler = new SemaphoreSlim(ThreadCount); 

        return Task.Run( // prevent UI thread lock
            async  () =>{
                foreach (var url in urls)
                {
                    // do an async wait until we can schedule again
                    await _throttler.WaitAsync();
                    ProccessUrl(url); // NOT await
                }
                //instead of await Task.WhenAll(allTasks);
                _countdownEvent.Wait();
            });
    }

    private async Task ProccessUrl(string url)
    {
        try
        {
            var page = await new WebClient()
                       .DownloadStringTaskAsync(new Uri(url)); 
            ProccessResult(page);
        }
        finally
        {
            _throttler.Release();
            _countdownEvent.Signal();
        }
    }

    private void ProccessResult(string page){/*....*/}
}

0voto

scottm Points 13578

Bien que 1000 tâches puissent être mises en file d'attente très rapidement, la bibliothèque Parallel Tasks ne peut gérer que des tâches simultanées égales au nombre de cœurs du processeur de la machine. Cela signifie que si vous avez une machine à quatre cœurs, seules 4 tâches seront exécutées à un moment donné (à moins que vous ne réduisiez le MaxDegreeOfParallelism).

-1voto

symbiont Points 944

Ce n'est pas une bonne pratique car cela modifie une variable globale. ce n'est pas non plus une solution générale pour l'async. mais c'est facile pour toutes les instances de HttpClient, si c'est tout ce que vous recherchez. vous pouvez simplement essayer :

System.Net.ServicePointManager.DefaultConnectionLimit = 20;

-2voto

GregC Points 4679

Les calculs parallèles doivent être utilisés pour accélérer les opérations liées à l'unité centrale. Nous parlons ici d'opérations liées aux E/S. Votre implémentation doit être purement asynchrone à moins que vous ne surchargiez le noyau unique occupé de votre CPU multi-core.

EDITAR J'aime la suggestion faite par usr d'utiliser un "sémaphore asynchrone" ici.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X