Foraggio parallelo con lambda asincrono


138

Vorrei gestire una raccolta in parallelo, ma ho difficoltà a implementarla e spero quindi in un aiuto.

Il problema sorge se voglio chiamare un metodo contrassegnato come asincrono in C #, all'interno della lambda del ciclo parallelo. Per esempio:

var bag = new ConcurrentBag<object>();
Parallel.ForEach(myCollection, async item =>
{
  // some pre stuff
  var response = await GetData(item);
  bag.Add(response);
  // some post stuff
}
var count = bag.Count;

Il problema si verifica con il conteggio pari a 0, poiché tutti i thread creati sono effettivamente solo thread in background e la Parallel.ForEachchiamata non attende il completamento. Se rimuovo la parola chiave asincrona, il metodo è simile al seguente:

var bag = new ConcurrentBag<object>();
Parallel.ForEach(myCollection, item =>
{
  // some pre stuff
  var responseTask = await GetData(item);
  responseTask.Wait();
  var response = responseTask.Result;
  bag.Add(response);
  // some post stuff
}
var count = bag.Count;

Funziona, ma disabilita completamente la capacità di attesa e devo fare alcune gestioni manuali delle eccezioni .. (Rimosso per brevità).

Come posso implementare un Parallel.ForEachciclo che utilizza la parola chiave wait in lambda? È possibile?

Il prototipo del metodo Parallel.ForEach accetta un Action<T>parametro as, ma voglio che aspetti la mia lambda asincrona.


1
Presumo che tu intendessi rimuovere awaitdal await GetData(item)secondo blocco di codice in quanto produrrebbe un errore di compilazione così com'è.
Josh M.

Risposte:


188

Se vuoi solo un semplice parallelismo, puoi farlo:

var bag = new ConcurrentBag<object>();
var tasks = myCollection.Select(async item =>
{
  // some pre stuff
  var response = await GetData(item);
  bag.Add(response);
  // some post stuff
});
await Task.WhenAll(tasks);
var count = bag.Count;

Se hai bisogno di qualcosa di più complesso, dai un'occhiata al ForEachAsyncpost di Stephen Toub .


46
Probabilmente è necessario un meccanismo di limitazione. Ciò creerà immediatamente tutte le attività quante sono le voci che potrebbero finire in richieste di rete 10k e simili.
usr

10
@usr L'ultimo esempio nell'articolo di Stephen Toub lo affronta.
svick,

@svick Ero perplesso su quell'ultimo campione. Mi sembra che raggruppa un sacco di compiti per crearmi altri, ma tutti iniziano in massa.
Luke Puplett,

2
@LukePuplett Crea dopattività e ognuna di esse elabora in serie alcuni sottoinsiemi della raccolta di input.
svick,

4
@Afshin_Zavvar: se chiami Task.Runsenza awaitil risultato, allora stai semplicemente gettando lavoro ignifugo nel pool di thread. Questo è quasi sempre un errore.
Stephen Cleary,

74

È possibile utilizzare il ParallelForEachAsyncmetodo di estensione dal pacchetto NuGet di AsyncEnumerator :

using Dasync.Collections;

var bag = new ConcurrentBag<object>();
await myCollection.ParallelForEachAsync(async item =>
{
  // some pre stuff
  var response = await GetData(item);
  bag.Add(response);
  // some post stuff
}, maxDegreeOfParallelism: 10);
var count = bag.Count;

1
Questo è il tuo pacchetto? Ti ho visto pubblicare questo post in alcuni punti adesso? : D Oh aspetta .. il tuo nome è sul pacchetto: D +1
Piotr Kula

17
@ppumkin, sì, è mio. Ho visto questo problema più e più volte, quindi ho deciso di risolverlo nel modo più semplice possibile e di liberare anche altri dalla lotta :)
Serge Semenov

Grazie .. ha sicuramente senso e mi ha aiutato alla grande!
Piotr Kula,

2
hai un refuso: maxDegreeOfParallelism>maxDegreeOfParalellism
Shiran Dror

3
L'ortografia corretta è davvero maxDegreeOfParallelism, tuttavia c'è qualcosa nel commento di @ ShiranDror - nel tuo pacchetto hai chiamato la variabile maxDegreeOfParalellism per errore (e quindi il tuo codice citato non verrà compilato fino a quando non lo cambi ..)
BornToCode

17

Con SemaphoreSlimte puoi ottenere il controllo del parallelismo.

var bag = new ConcurrentBag<object>();
var maxParallel = 20;
var throttler = new SemaphoreSlim(initialCount: maxParallel);
var tasks = myCollection.Select(async item =>
{
  try
  {
     await throttler.WaitAsync();
     var response = await GetData(item);
     bag.Add(response);
  }
  finally
  {
     throttler.Release();
  }
});
await Task.WhenAll(tasks);
var count = bag.Count;

3

La mia implementazione leggera di ParallelForEach async.

Caratteristiche:

  1. Limitazione (massimo grado di parallelismo).
  2. Gestione delle eccezioni (l'eccezione di aggregazione verrà generata al completamento).
  3. Memoria efficiente (non è necessario memorizzare l'elenco di attività).

public static class AsyncEx
{
    public static async Task ParallelForEachAsync<T>(this IEnumerable<T> source, Func<T, Task> asyncAction, int maxDegreeOfParallelism = 10)
    {
        var semaphoreSlim = new SemaphoreSlim(maxDegreeOfParallelism);
        var tcs = new TaskCompletionSource<object>();
        var exceptions = new ConcurrentBag<Exception>();
        bool addingCompleted = false;

        foreach (T item in source)
        {
            await semaphoreSlim.WaitAsync();
            asyncAction(item).ContinueWith(t =>
            {
                semaphoreSlim.Release();

                if (t.Exception != null)
                {
                    exceptions.Add(t.Exception);
                }

                if (Volatile.Read(ref addingCompleted) && semaphoreSlim.CurrentCount == maxDegreeOfParallelism)
                {
                    tcs.SetResult(null);
                }
            });
        }

        Volatile.Write(ref addingCompleted, true);
        await tcs.Task;
        if (exceptions.Count > 0)
        {
            throw new AggregateException(exceptions);
        }
    }
}

Esempio di utilizzo:

await Enumerable.Range(1, 10000).ParallelForEachAsync(async (i) =>
{
    var data = await GetData(i);
}, maxDegreeOfParallelism: 100);

2

Ho creato un metodo di estensione per questo che utilizza SemaphoreSlim e consente anche di impostare il massimo grado di parallelismo

    /// <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="maxDegreeOfParallelism">Optional, An integer that represents the maximum degree of parallelism,
    /// 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? maxDegreeOfParallelism = null)
    {
        if (maxDegreeOfParallelism.HasValue)
        {
            using (var semaphoreSlim = new SemaphoreSlim(
                maxDegreeOfParallelism.Value, maxDegreeOfParallelism.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 tasks to complete.
                await Task.WhenAll(tasksWithThrottler.ToArray());
            }
        }
        else
        {
            await Task.WhenAll(enumerable.Select(item => action(item)));
        }
    }

Esempio di utilizzo:

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

'utilizzo' non aiuterà. foreach loop attenderà indefinitamente il semafono. Basta provare questo semplice codice che riproduce il problema: wait Enumerable.Range (1, 4). 2);
nicolay.anykienko,

@ nicolay.anykienko hai ragione circa # 2. Questo problema di memoria può essere risolto aggiungendo taskWithThrottler.RemoveAll (x => x.IsCompleted);
askids

1
L'ho provato nel mio codice e se maxDegreeOfParallelism non è nullo, i deadlock del codice. Qui potete vedere tutto il codice per riprodurre: stackoverflow.com/questions/58793118/...
Massimo Savazzi
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.