¿Cómo puedo usar Async con ForEach?

123

¿Es posible usar Async cuando se usa ForEach? A continuación se muestra el código que estoy probando:

using (DataContext db = new DataLayer.DataContext())
{
    db.Groups.ToList().ForEach(i => async {
        await GetAdminsFromGroup(i.Gid);
    });
}

Obtengo el error:

El nombre 'Async' no existe en el contexto actual

El método en el que se incluye la instrucción using se establece en async.

James Jeffery
fuente

Respuestas:

180

List<T>.ForEachno funciona particularmente bien con async(tampoco LINQ-to-objects, por las mismas razones).

En este caso, recomiendo proyectar cada elemento en una operación asincrónica, y luego puede (asincrónicamente) esperar a que se completen todos.

using (DataContext db = new DataLayer.DataContext())
{
    var tasks = db.Groups.ToList().Select(i => GetAdminsFromGroupAsync(i.Gid));
    var results = await Task.WhenAll(tasks);
}

Los beneficios de este enfoque sobre la asignación de un asyncdelegado ForEachson:

  1. El manejo de errores es más adecuado. Las excepciones de async voidno se pueden atrapar catch; este enfoque propagará las excepciones en la await Task.WhenAlllínea, lo que permitirá el manejo natural de excepciones.
  2. Sabes que las tareas están completas al final de este método, ya que hace un await Task.WhenAll. Si lo usa async void, no puede saber fácilmente cuándo se han completado las operaciones.
  3. Este enfoque tiene una sintaxis natural para recuperar los resultados. GetAdminsFromGroupAsyncParece que es una operación que produce un resultado (los administradores), y dicho código es más natural si dichas operaciones pueden devolver sus resultados en lugar de establecer un valor como efecto secundario.
Stephen Cleary
fuente
5
No es que cambie nada, pero List.ForEach()no es parte de LINQ.
svick
Gran sugerencia @StephenCleary y gracias por todas las respuestas que has dado async. ¡Han sido de gran ayuda!
Justin Helgerson
4
@StewartAnderson: las tareas se ejecutarán al mismo tiempo. No hay extensión para la ejecución en serie; solo haz un foreachcon un awaiten tu cuerpo de bucle.
Stephen Cleary
1
@mare: ForEachsolo toma un tipo de delegado síncrono y no hay sobrecarga al tomar un tipo de delegado asincrónico. Entonces, la respuesta corta es "nadie escribió un asincrónico ForEach". La respuesta más larga es que tendrías que asumir algo de semántica; Por ejemplo, ¿deberían procesarse los artículos uno a la vez (comoforeach ) o simultáneamente (como Select)? Si se trata de uno a la vez, ¿no serían las transmisiones asincrónicas una mejor solución? Si es simultáneo, ¿deberían los resultados estar en el orden del artículo original o en el orden de finalización? ¿Debería fallar en la primera falla o esperar hasta que todos se hayan completado? Etc.
Stephen Cleary
2
@RogerWolf: Sí; Úselo SemaphoreSlimpara acelerar las tareas asincrónicas.
Stephen Cleary
61

Este pequeño método de extensión debería brindarle una iteración asíncrona segura para excepciones:

public static async Task ForEachAsync<T>(this List<T> list, Func<T, Task> func)
{
    foreach (var value in list)
    {
        await func(value);
    }
}

Dado que estamos cambiando el tipo de retorno de la lambda de void a Task, las excepciones se propagarán correctamente. Esto le permitirá escribir algo como esto en la práctica:

await db.Groups.ToList().ForEachAsync(async i => {
    await GetAdminsFromGroup(i.Gid);
});
JD Courtoy
fuente
Creo que asyncdebería ser antesi =>
Todd
En lugar de esperar a ForEachAsyn (), también se podría llamar a Wait ().
Jonas
No es necesario esperar aquí a Lambda.
hazzik
Agregaría
El ForEachAsynces esencialmente un método de biblioteca, por lo que la espera probablemente debería configurarse con ConfigureAwait(false).
Theodor Zoulias
9

La respuesta simple es usar la foreachpalabra clave en lugar del ForEach()método de List().

using (DataContext db = new DataLayer.DataContext())
{
    foreach(var i in db.Groups)
    {
        await GetAdminsFromGroup(i.Gid);
    }
}
RubberDuck
fuente
Eres un genio
Vick_onrails
8

Aquí hay una versión de trabajo real de las variantes de foreach asíncronas anteriores con procesamiento secuencial:

public static async Task ForEachAsync<T>(this List<T> enumerable, Action<T> action)
{
    foreach (var item in enumerable)
        await Task.Run(() => { action(item); }).ConfigureAwait(false);
}

Aquí está la implementación:

public async void SequentialAsync()
{
    var list = new List<Action>();

    Action action1 = () => {
        //do stuff 1
    };

    Action action2 = () => {
        //do stuff 2
    };

    list.Add(action1);
    list.Add(action2);

    await list.ForEachAsync();
}

¿Cuál es la diferencia clave? .ConfigureAwait(false);que mantiene el contexto del hilo principal mientras el procesamiento secuencial asincrónico de cada tarea.

mrogunlana
fuente
6

A partir de C# 8.0, puede crear y consumir transmisiones de forma asincrónica.

    private async void button1_Click(object sender, EventArgs e)
    {
        IAsyncEnumerable<int> enumerable = GenerateSequence();

        await foreach (var i in enumerable)
        {
            Debug.WriteLine(i);
        }
    }

    public static async IAsyncEnumerable<int> GenerateSequence()
    {
        for (int i = 0; i < 20; i++)
        {
            await Task.Delay(100);
            yield return i;
        }
    }

Más

Andrei Krasutski
fuente
1
Esto tiene la ventaja de que además de esperar cada elemento, ahora también está esperando el MoveNextdel enumerador. Esto es importante en los casos en que el enumerador no puede buscar el siguiente elemento instantáneamente y debe esperar a que haya uno disponible.
Theodor Zoulias
3

Agregar este método de extensión

public static class ForEachAsyncExtension
{
    public static Task ForEachAsync<T>(this IEnumerable<T> source, int dop, Func<T, Task> body)
    {
        return Task.WhenAll(from partition in Partitioner.Create(source).GetPartitions(dop) 
            select Task.Run(async delegate
            {
                using (partition)
                    while (partition.MoveNext())
                        await body(partition.Current).ConfigureAwait(false);
            }));
    }
}

Y luego usa así:

Task.Run(async () =>
{
    var s3 = new AmazonS3Client(Config.Instance.Aws.Credentials, Config.Instance.Aws.RegionEndpoint);
    var buckets = await s3.ListBucketsAsync();

    foreach (var s3Bucket in buckets.Buckets)
    {
        if (s3Bucket.BucketName.StartsWith("mybucket-"))
        {
            log.Information("Bucket => {BucketName}", s3Bucket.BucketName);

            ListObjectsResponse objects;
            try
            {
                objects = await s3.ListObjectsAsync(s3Bucket.BucketName);
            }
            catch
            {
                log.Error("Error getting objects. Bucket => {BucketName}", s3Bucket.BucketName);
                continue;
            }

            // ForEachAsync (4 is how many tasks you want to run in parallel)
            await objects.S3Objects.ForEachAsync(4, async s3Object =>
            {
                try
                {
                    log.Information("Bucket => {BucketName} => {Key}", s3Bucket.BucketName, s3Object.Key);
                    await s3.DeleteObjectAsync(s3Bucket.BucketName, s3Object.Key);
                }
                catch
                {
                    log.Error("Error deleting bucket {BucketName} object {Key}", s3Bucket.BucketName, s3Object.Key);
                }
            });

            try
            {
                await s3.DeleteBucketAsync(s3Bucket.BucketName);
            }
            catch
            {
                log.Error("Error deleting bucket {BucketName}", s3Bucket.BucketName);
            }
        }
    }
}).Wait();
superlógico
fuente
2

El problema es que la asyncpalabra clave debe aparecer antes del lambda, no antes del cuerpo:

db.Groups.ToList().ForEach(async (i) => {
    await GetAdminsFromGroup(i.Gid);
});
James Jeffery
fuente
35
-1 para el uso innecesario y sutil de async void. Este enfoque tiene problemas en torno al manejo de excepciones y a saber cuándo se completan las operaciones asincrónicas.
Stephen Cleary
Sí, encontré que esto no maneja las excepciones correctamente.
Herman Schoenfeld