web-dev-qa-db-fra.com

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 démarre plus de 1000 requêtes Web simultanées. Existe-t-il un moyen simple de limiter le nombre simultané de ces requêtes http asynchrones? Afin que pas plus de 20 pages Web soient téléchargées à tout moment. Comment le faire de la manière la plus efficace?

95
Grief Coder

Vous pouvez certainement le faire dans les dernières versions d'async pour .NET, en utilisant .NET 4.5 Beta. Le post précédent de "usr" pointe vers un bon article écrit par Stephen Toub, mais les nouvelles moins annoncées sont que le sémaphore asynchrone a effectivement été intégré dans la version bêta de .NET 4.5

Si vous regardez notre classe SemaphoreSlim bien-aimée (que vous devriez utiliser car elle est plus performante que l'original Semaphore ), c'est maintenant possède la série WaitAsync(...) de surcharges, avec tous les arguments attendus - intervalles de temporisation, jetons d'annulation, tous vos amis de planification habituels :)

Stephen a également écrit un article de blog plus récent sur les nouveaux goodies .NET 4.5 qui sont sortis avec la version bêta voir Quoi de neuf pour le parallélisme dans .NET 4.5 Beta .

Enfin, voici un exemple de code sur la façon d'utiliser SemaphoreSlim pour la limitation de la méthode asynchrone:

public async Task MyOuterMethod()
{
    // let's say there is a list of 1000+ URLs
    var urls = { "http://google.com", "http://yahoo.com", ... };

    // now let's send HTTP requests to each of these URLs in parallel
    var allTasks = new List<Task>();
    var throttler = new SemaphoreSlim(initialCount: 20);
    foreach (var url in urls)
    {
        // do an async wait until we can schedule again
        await throttler.WaitAsync();

        // using Task.Run(...) to run the lambda in its own parallel
        // flow on the threadpool
        allTasks.Add(
            Task.Run(async () =>
            {
                try
                {
                    var client = new HttpClient();
                    var html = await client.GetStringAsync(url);
                }
                finally
                {
                    throttler.Release();
                }
            }));
    }

    // won't get here until all urls have been put into tasks
    await Task.WhenAll(allTasks);

    // won't get here until all tasks have completed in some way
    // (either success or exception)
}

Dernier point, mais probablement une mention digne est une solution qui utilise la planification basée sur TPL. Vous pouvez créer des tâches liées au délégué sur le TPL qui n'ont pas encore été démarrées et autoriser un planificateur de tâches personnalisé pour limiter la simultanéité. En fait, il y a un exemple MSDN pour cela ici:

Voir aussi TaskScheduler .

139
Theo Yaung

Si vous avez un IEnumerable (c.-à-d. Des chaînes d'URL) et que vous souhaitez effectuer une opération liée aux E/S avec chacun d'entre eux (c.-à-d. Faire une demande http asynchrone) simultanément ET éventuellement vous souhaitez également définir le nombre maximal de simultanés Demandes d'E/S en temps réel, voici comment procéder. De cette façon, vous n'utilisez pas de pool de threads et autres, la méthode utilise semaphoreslim pour contrôler le nombre maximal de demandes d'E/S simultanées, semblable à un modèle de fenêtre coulissante qu'une demande termine, quitte le sémaphore et le suivant entre.

utilisation: attendre ForEachAsync (urlStrings, YourAsyncFunc, optionalMaxDegreeOfConcurrency);

public static Task ForEachAsync<TIn>(
        IEnumerable<TIn> inputEnumerable,
        Func<TIn, Task> asyncProcessor,
        int? maxDegreeOfParallelism = null)
    {
        int maxAsyncThreadCount = maxDegreeOfParallelism ?? DefaultMaxDegreeOfParallelism;
        SemaphoreSlim throttler = new SemaphoreSlim(maxAsyncThreadCount, maxAsyncThreadCount);

        IEnumerable<Task> tasks = inputEnumerable.Select(async input =>
        {
            await throttler.WaitAsync().ConfigureAwait(false);
            try
            {
                await asyncProcessor(input).ConfigureAwait(false);
            }
            finally
            {
                throttler.Release();
            }
        });

        return Task.WhenAll(tasks);
    }
10
Dogu Arslan

Malheureusement, le .NET Framework manque la plupart des combinateurs importants pour orchestrer les tâches asynchrones parallèles. Il n'y a rien de tel intégré.

Regardez la classe AsyncSemaphore construite par le plus respectable Stephen Toub. Ce que vous voulez s'appelle un sémaphore, et vous en avez besoin d'une version asynchrone.

8
usr

Il y a beaucoup d'embûches et l'utilisation directe d'un sémaphore peut être délicate dans les cas d'erreur, je suggère donc d'utiliser AsyncEnumerator NuGet Package au lieu de réinventer la roue:

// 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
await urls.ParallelForEachAsync(async (url) => {
    var client = new HttpClient();
    var html = await client.GetStringAsync(url);
}, maxDegreeOfParalellism: 20);
5
Serge Semenov

L'exemple de Theo Yaung est Nice, 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){/*....*/}
}
4
vitidev

SemaphoreSlim peut être très utile ici. Voici la méthode d'extension que j'ai créée.

    /// <summary>
    /// Concurrently Executes async actions for each item of <see cref="IEnumerable<typeparamref name="T"/>
    /// </summary>
    /// <typeparam name="T">Type of IEnumerable</typeparam>
    /// <param name="enumerable">instance of <see cref="IEnumerable<typeparamref name="T"/>"/></param>
    /// <param name="action">an async <see cref="Action" /> to execute</param>
    /// <param name="maxActionsToRunInParallel">Optional, max numbers of the actions to run in parallel,
    /// Must be grater than 0</param>
    /// <returns>A Task representing an async operation</returns>
    /// <exception cref="ArgumentOutOfRangeException">If the maxActionsToRunInParallel is less than 1</exception>
    public static async Task ForEachAsyncConcurrent<T>(
        this IEnumerable<T> enumerable,
        Func<T, Task> action,
        int? maxActionsToRunInParallel = null)
    {
        if (maxActionsToRunInParallel.HasValue)
        {
            using (var semaphoreSlim = new SemaphoreSlim(
                maxActionsToRunInParallel.Value, maxActionsToRunInParallel.Value))
            {
                var tasksWithThrottler = new List<Task>();

                foreach (var item in enumerable)
                {
                    // Increment the number of currently running tasks and wait if they are more than limit.
                    await semaphoreSlim.WaitAsync();

                    tasksWithThrottler.Add(Task.Run(async () =>
                    {
                        await action(item).ContinueWith(res =>
                        {
                            // action is completed, so decrement the number of currently running tasks
                            semaphoreSlim.Release();
                        });
                    }));
                }

                // Wait for all of the provided tasks to complete.
                await Task.WhenAll(tasksWithThrottler.ToArray());
            }
        }
        else
        {
            await Task.WhenAll(enumerable.Select(item => action(item)));
        }
    }

Exemple d'utilisation:

await enumerable.ForEachAsyncConcurrent(
    async item =>
    {
        await SomeAsyncMethod(item);
    },
    5);
2
Jay Shah

Juste une version plus succincte de https://stackoverflow.com/a/10810730/1186165 :

static async Task WhenAll(IEnumerable<Task> tasks, int maxThreadCount) {
    using (var guard = new SemaphoreSlim(initialCount: maxThreadCount)) {
        await Task.WhenAll(tasks.Select(async task => {
            await guard.WaitAsync();

            return task.ContinueWith(t => guard.Release());
        }));
    }
}
2
Kittoes0124

Voici une solution qui tire parti de la nature paresseuse de LINQ. Il offre l'avantage de ne pas générer de threads (comme le réponse acceptée le fait), et de ne pas avoir toutes les tâches créées en même temps et presque toutes bloquées sur un SemaphoreSlim, comme le SemaphoreSlim solutions. Permet d'abord de le faire fonctionner sans étranglement. La première étape consiste à convertir nos URL en un énumérable de tâches.

string[] urls =
{
    "https://stackoverflow.com",
    "https://superuser.com",
    "https://serverfault.com",
    "https://meta.stackexchange.com",
    // ...
};
var httpClient = new HttpClient();
var tasks = urls.Select(async (url) =>
{
    return (Url: url, Html: await httpClient.GetStringAsync(url));
});

La deuxième étape consiste à await toutes les tâches simultanément en utilisant Task.WhenAll méthode:

var results = await Task.WhenAll(tasks);
foreach (var result in results)
{
    Console.WriteLine($"Url: {result.Url}, {result.Html.Length:#,0} chars");
}

Sortie:

URL: https://stackoverflow.com , 105,574 caractères
URL: https://superuser.com , 126,953 caractères
URL: https://serverfault.com , 125,963 caractères
URL: https://meta.stackexchange.com , 185,276 caractères
...

implémentation de Microsoft of Task.WhenAll matérialise instantanément l'énumérable fourni dans un tableau, provoquant le démarrage simultané de toutes les tâches. Nous ne voulons pas cela, car nous voulons limiter le nombre d'opérations asynchrones simultanées. Nous aurons donc besoin d'implémenter une alternative WhenAll qui énumérera notre énumérable doucement et lentement. Nous le ferons en créant un certain nombre de tâches de travail (égal au degré de parallélisme souhaité), et chaque tâche de travail énumérera notre énumérable une tâche à la fois, en utilisant un verrou pour garantir que chaque tâche d'URL sera traitée par une seule tâche ouvrière. Ensuite, nous await pour que toutes les tâches de travail soient terminées, et enfin nous renvoyons les résultats après avoir restauré leur ordre. Voici l'implémentation:

public static async Task<T[]> WhenAll<T>(IEnumerable<Task<T>> tasks,
    int degreeOfParallelism)
{
    if (tasks is ICollection<Task<T>>) throw new ArgumentException(
        "The enumerable should not be materialized.", nameof(tasks));
    var results = new List<(int Index, T Result)>();
    var failed = false;
    using (var enumerator = tasks.GetEnumerator())
    {
        int index = 0;
        var workerTasks = Enumerable.Range(0, degreeOfParallelism)
        .Select(async _ =>
        {
            try
            {
                while (true)
                {
                    Task<T> task;
                    int localIndex;
                    lock (enumerator)
                    {
                        if (failed || !enumerator.MoveNext()) break;
                        task = enumerator.Current;
                        localIndex = index++;
                    }
                    var result = await task.ConfigureAwait(false);
                    lock (results) results.Add((localIndex, result));
                }
            }
            catch
            {
                lock (enumerator) failed = true;
                throw;
            }
        }).ToArray();
        await Task.WhenAll(workerTasks).ConfigureAwait(false);
    }
    return results.OrderBy(e => e.Index).Select(e => e.Result).ToArray();
}

... et voici ce que nous devons changer dans notre code initial, pour atteindre la limitation souhaitée:

var results = await WhenAll(tasks, degreeOfParallelism: 2);

Il existe une différence concernant le traitement des exceptions. Le natif Task.WhenAll attend que toutes les tâches soient terminées et agrège toutes les exceptions. L'implémentation ci-dessus cesse d'attendre peu de temps après la fin de la première tâche défaillante.

0
Theodor Zoulias

Utilisez MaxDegreeOfParallelism, qui est une option que vous pouvez spécifier dans Parallel.ForEach() :

var options = new ParallelOptions { MaxDegreeOfParallelism = 20 };

Parallel.ForEach(urls, options,
    url =>
        {
            var client = new HttpClient();
            var html = client.GetStringAsync(url);
            // do stuff with html
        });
0
Sean U

Ancienne question, nouvelle réponse. @vitidev avait un bloc de code qui a été réutilisé presque intact dans un projet que j'ai examiné. Après avoir discuté avec quelques collègues, l'un d'eux a demandé: "Pourquoi n'utilisez-vous pas simplement les méthodes TPL intégrées?" ActionBlock ressemble au gagnant là-bas. https://msdn.Microsoft.com/en-us/library/hh194773 (v = vs.110) .aspx . Ne changera probablement pas de code existant, mais cherchera certainement à adopter ce nuget et à réutiliser la meilleure pratique de M. Softy pour le parallélisme limité.

0

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

0
scottm

Des calculs parallèles doivent être utilisés pour accélérer les opérations liées au processeur. Ici, nous parlons des opérations liées aux E/S. Votre implémentation doit être purement asynchrone , à moins que vous n'écrasiez le cœur unique occupé sur votre processeur multicœur.

[~ # ~] modifier [~ # ~] J'aime la suggestion faite par usr d'utiliser ici un "sémaphore asynchrone".

0
GregC