Como posso usar o Async com ForEach?

123

É possível usar Async ao usar ForEach? Abaixo está o código que estou tentando:

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

Estou recebendo o erro:

O nome 'Async' não existe no contexto atual

O método em que a instrução using está incluída é definido como assíncrono.

James Jeffery
fonte

Respostas:

180

List<T>.ForEachnão funciona muito bem com async(nem o LINQ-to-objects, pelos mesmos motivos).

Nesse caso, recomendo projetar cada elemento em uma operação assíncrona, e você pode então (de forma assíncrona) esperar que todos eles sejam concluídos.

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

Os benefícios dessa abordagem sobre dar um asyncdelegado ForEachsão:

  1. O tratamento de erros é mais adequado. As exceções de async voidnão podem ser detectadas com catch; esta abordagem propagará exceções na await Task.WhenAlllinha, permitindo o tratamento de exceção natural.
  2. Você sabe que as tarefas estão concluídas ao final deste método, pois ele faz um await Task.WhenAll. Se você usar async void, não será possível dizer facilmente quando as operações foram concluídas.
  3. Essa abordagem tem uma sintaxe natural para recuperar os resultados. GetAdminsFromGroupAsyncparece que é uma operação que produz um resultado (os administradores), e esse código é mais natural se essas operações puderem retornar seus resultados em vez de definir um valor como efeito colateral.
Stephen Cleary
fonte
5
Não que isso mude nada, mas List.ForEach()não faz parte do LINQ.
svick
Ótima sugestão @StephenCleary e obrigado por todas as respostas que você deu async. Eles têm sido muito úteis!
Justin Helgerson
4
@StewartAnderson: As tarefas serão executadas simultaneamente. Não há extensão para execução serial; basta fazer um foreachcom um awaitem seu corpo de loop.
Stephen Cleary
1
@mare: ForEachaceita apenas um tipo de delegado síncrono e não há sobrecarga em um tipo de delegado assíncrono. Portanto, a resposta curta é "ninguém escreveu um assíncrono ForEach". A resposta mais longa é que você teria que assumir alguma semântica; por exemplo, os itens devem ser processados ​​um de cada vez (como foreach) ou simultaneamente (como Select)? Se um de cada vez, os fluxos assíncronos não seriam uma solução melhor? Se simultaneamente, os resultados devem estar na ordem do item original ou na ordem de conclusão? Deve falhar na primeira falha ou esperar até que tudo seja concluído? Etc.
Stephen Cleary
2
@RogerWolf: Sim; use SemaphoreSlimpara controlar tarefas assíncronas.
Stephen Cleary
61

Este pequeno método de extensão deve fornecer iteração assíncrona segura com exceção:

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

Como estamos alterando o tipo de retorno do lambda de voidpara Task, as exceções serão propagadas corretamente. Isso permitirá que você escreva algo assim na prática:

await db.Groups.ToList().ForEachAsync(async i => {
    await GetAdminsFromGroup(i.Gid);
});
JD Courtoy
fonte
Eu acredito que asyncdeveria ser antesi =>
Todd,
Em vez de aguardar ForEachAsyn (), também se pode chamar Wait ().
Jonas
Lambda não precisa ser aguardado aqui.
Hazzik,
Eu adicionaria suporte para CancelamentoToken como na Resposta de Todd aqui stackoverflow.com/questions/29787098/…
Zorkind
O ForEachAsyncé essencialmente um método de biblioteca, portanto, o awaiting provavelmente deve ser configurado com ConfigureAwait(false).
Theodor Zoulias
9

A resposta simples é usar a foreachpalavra - chave em vez do ForEach()método de List().

using (DataContext db = new DataLayer.DataContext())
{
    foreach(var i in db.Groups)
    {
        await GetAdminsFromGroup(i.Gid);
    }
}
Pato de borracha
fonte
Você é um gênio
Vick_onrails
8

Aqui está uma versão funcional real das variantes de foreach assíncronas acima com processamento sequencial:

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

Aqui está a implementação:

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

Qual é a principal diferença? .ConfigureAwait(false);que mantém o contexto do thread principal durante o processamento sequencial assíncrono de cada tarefa.

mrogunlana
fonte
6

Começando com C# 8.0, você pode criar e consumir fluxos de forma assíncrona.

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

Mais

Andrei Krasutski
fonte
1
A vantagem disso é que além de aguardar cada elemento, agora você também está aguardando o MoveNextdo enumerador. Isso é importante nos casos em que o enumerador não pode buscar o próximo elemento instantaneamente e deve esperar que um fique disponível.
Theodor Zoulias
3

Adicionar este método de extensão

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

E então use assim:

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
fonte
2

O problema era que a asyncpalavra - chave precisava aparecer antes do lambda, não antes do corpo:

db.Groups.ToList().ForEach(async (i) => {
    await GetAdminsFromGroup(i.Gid);
});
James Jeffery
fonte
35
-1 para uso desnecessário e sutil de async void. Essa abordagem tem problemas em torno do tratamento de exceções e de saber quando as operações assíncronas são concluídas.
Stephen Cleary
Sim, descobri que isso não lida com exceções corretamente.
Herman Schoenfeld