Foreach paralelo con lambda asincrónica


138

Me gustaría manejar una colección en paralelo, pero tengo problemas para implementarla y, por lo tanto, espero ayuda.

El problema surge si quiero llamar a un método marcado asíncrono en C #, dentro de la lambda del bucle paralelo. Por ejemplo:

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;

El problema ocurre cuando el recuento es 0, porque todos los subprocesos creados son efectivamente solo subprocesos en segundo plano y la Parallel.ForEachllamada no espera su finalización. Si elimino la palabra clave asíncrona, el método se ve así:

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;

Funciona, pero deshabilita por completo la agudeza de espera y tengo que hacer un manejo manual de excepciones. (Eliminado por brevedad).

¿Cómo puedo implementar un Parallel.ForEachbucle que usa la palabra clave wait dentro de lambda? ¿Es posible?

El prototipo del método Parallel.ForEach toma un Action<T>parámetro como, pero quiero que espere mi lambda asíncrona.


1
Asumo que quería decir para quitar awaitde await GetData(item)en su segundo bloque de código, ya que produciría un error de compilación como está.
Josh M.

Respuestas:


186

Si solo quieres un paralelismo simple, puedes hacer esto:

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;

Si necesitas algo más complejo, mira la ForEachAsyncpublicación de Stephen Toub .


46
Probablemente se necesita un mecanismo de estrangulamiento. Esto creará inmediatamente tantas tareas como elementos que puedan terminar en solicitudes de red de 10k y tal.
Usr

10
@usr El último ejemplo en el artículo de Stephen Toub aborda eso.
svick

@svick Estaba desconcertando esa última muestra. Me parece que solo agrupa un montón de tareas para crearme más tareas, pero todas comienzan en masa.
Luke Puplett

2
@LukePuplett Crea doptareas y cada una de ellas procesa algún subconjunto de la colección de entrada en serie.
svick

44
@Afshin_Zavvar: si llamas Task.Runsin obtener awaitel resultado, entonces eso es solo lanzar trabajo de disparar y olvidar en el grupo de subprocesos. Eso casi siempre es un error.
Stephen Cleary

74

Puede usar el ParallelForEachAsyncmétodo de extensión del paquete AsyncEnumerator NuGet :

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
Este es tu paquete? ¿Te he visto publicar esto en algunos lugares ahora? : D Oh, espera ... tu nombre está en el paquete: D +1
Piotr Kula

17
@ppumkin, sí, es mío. He visto este problema una y otra vez, así que decidí resolverlo de la manera más simple posible y liberar a otros de la lucha también :)
Serge Semenov

Gracias ... definitivamente tiene sentido y me ayudó a lo grande!
Piotr Kula

2
tienes un error tipográfico: maxDegreeOfParallelism>maxDegreeOfParalellism
Shiran Dror

3
La ortografía correcta es de hecho maxDegreeOfParallelism, sin embargo, hay algo en el comentario de @ ShiranDror: en su paquete llamó a la variable maxDegreeOfParalellism por error (y, por lo tanto, su código citado no se compilará hasta que lo cambie ...)
BornToCode

17

Con SemaphoreSlimusted puede lograr el control de paralelismo.

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

Mi implementación ligera de ParallelForEach async.

caracteristicas:

  1. Aceleración (grado máximo de paralelismo).
  2. Manejo de excepciones (la excepción de agregación se generará al finalizar).
  3. Memoria eficiente (no es necesario almacenar la lista de tareas).

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

Ejemplo de uso:

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

2

He creado un método de extensión para esto que hace uso de SemaphoreSlim y también permite establecer el grado máximo de paralelismo

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

Uso de muestra:

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

'usar' no ayudará. foreach loop estará esperando el semáfono indefinidamente. Simplemente pruebe este código simple que reproduce el problema: aguarde Enumerable.Range (1, 4) .ForEachAsyncConcurrent (async (i) => {Console.WriteLine (i); arroje una nueva excepción ("excepción de prueba");}, maxDegreeOfParallelism: 2);
nicolay.anykienko

@ nicolay.anykienko tienes razón sobre el # 2. Ese problema de memoria se puede resolver agregando tareasWithThrottler.RemoveAll (x => x.IsCompleted);
askids

1
Lo he intentado en mi código y si maxDegreeOfParallelism no es nulo, los puntos muertos del código. Aquí puedes ver todo el código para reproducir: stackoverflow.com/questions/58793118/…
Massimo Savazzi
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.