Async aguardam no linq select

180

Preciso modificar um programa existente e ele contém o seguinte código:

var inputs = events.Select(async ev => await ProcessEventAsync(ev))
                   .Select(t => t.Result)
                   .Where(i => i != null)
                   .ToList();

Mas isso me parece muito estranho, antes de tudo, o uso de asynce awaitno select. De acordo com esta resposta de Stephen Cleary, eu deveria ser capaz de abandoná-las.

Então o segundo Selectque seleciona o resultado. Isso não significa que a tarefa não é assíncrona e é executada de forma síncrona (muito esforço por nada) ou será executada de forma assíncrona e quando concluída, o restante da consulta é executado?

Devo escrever o código acima como segue, de acordo com outra resposta de Stephen Cleary :

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

e é completamente o mesmo assim?

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
                                       .Where(result => result != null).ToList();

Enquanto estou trabalhando neste projeto, gostaria de alterar o primeiro exemplo de código, mas não estou muito interessado em alterar (aparentemente trabalhando) código assíncrono. Talvez eu esteja apenas me preocupando por nada e todos os três exemplos de código façam exatamente a mesma coisa?

ProcessEventsAsync tem esta aparência:

async Task<InputResult> ProcessEventAsync(InputEvent ev) {...}
Alexander Derck
fonte
Qual é o tipo de retorno de ProceesEventAsync?
precisa saber é
@ tede24 É Task<InputResult>como InputResultser uma classe personalizada.
Alexander Derck
Suas versões são muito mais fáceis de ler na minha opinião. No entanto, você esqueceu Selectos resultados das tarefas anteriores à sua Where.
Max
E InputResult tem uma propriedade Result, certo?
precisa saber é
@ tede24 O resultado é propriedade da tarefa e não da minha classe. E o @Max the wait deve garantir que eu obtenha os resultados sem acessar a Resultpropriedade da tarefa #
Alexander Derck

Respostas:

184
var inputs = events.Select(async ev => await ProcessEventAsync(ev))
                   .Select(t => t.Result)
                   .Where(i => i != null)
                   .ToList();

Mas isso me parece muito estranho, antes de tudo o uso de assíncrono e aguardo no select. De acordo com esta resposta de Stephen Cleary, eu deveria ser capaz de abandoná-las.

A chamada para Selecté válida. Essas duas linhas são essencialmente idênticas:

events.Select(async ev => await ProcessEventAsync(ev))
events.Select(ev => ProcessEventAsync(ev))

(Há uma pequena diferença em relação a como uma exceção síncrona seria lançada ProcessEventAsync, mas no contexto desse código isso não importa.)

Em seguida, o segundo Select que seleciona o resultado. Isso não significa que a tarefa não é assíncrona e é executada de forma síncrona (muito esforço por nada) ou será executada de forma assíncrona e quando concluída, o restante da consulta é executado?

Isso significa que a consulta está bloqueando. Portanto, não é realmente assíncrono.

Dividindo:

var inputs = events.Select(async ev => await ProcessEventAsync(ev))

primeiro iniciará uma operação assíncrona para cada evento. Então esta linha:

                   .Select(t => t.Result)

esperará que essas operações sejam concluídas uma de cada vez (primeiro aguarda a operação do primeiro evento, depois o próximo, depois o próximo, etc.).

Esta é a parte da qual não me importo, porque bloqueia e também inclui exceções AggregateException.

e é completamente o mesmo assim?

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
                                       .Where(result => result != null).ToList();

Sim, esses dois exemplos são equivalentes. Os dois iniciam todas as operações assíncronas ( events.Select(...)), aguardam de forma assíncrona que todas as operações sejam concluídas em qualquer ordem ( await Task.WhenAll(...)) e depois prosseguem com o restante do trabalho ( Where...).

Ambos os exemplos são diferentes do código original. O código original está bloqueando e incluirá exceções AggregateException.

Stephen Cleary
fonte
Felicidades por esclarecer isso! Então, em vez das exceções agrupadas em um AggregateExceptioneu obteria várias exceções separadas no segundo código?
Alexander Derck
1
@AlexanderDerck: Não, no código antigo e no novo, apenas a primeira exceção seria levantada. Mas com Resultisso seria envolvido AggregateException.
Stephen Cleary
Estou recebendo um impasse no meu controlador ASP.NET MVC usando esse código. Eu o resolvi usando o Task.Run (…). Eu não tenho um bom pressentimento sobre isso. No entanto, ele foi finalizado corretamente ao executar um teste assíncrono xUnit. O que está acontecendo?
21719 SuperJMN
2
@SuperJMN: Replace stuff.Select(x => x.Result);withawait Task.WhenAll(stuff)
Stephen Cleary
1
@ Daniel: Eles são essencialmente os mesmos. Existem algumas diferenças, como máquinas de estado, contexto de captura, comportamento de exceções síncronas. Mais informações em blog.stephencleary.com/2016/12/eliding-async-await.html
Stephen Cleary
25

O código existente está funcionando, mas está bloqueando o encadeamento.

.Select(async ev => await ProcessEventAsync(ev))

cria uma nova tarefa para cada evento, mas

.Select(t => t.Result)

bloqueia o encadeamento aguardando o término de cada nova tarefa.

Por outro lado, seu código produz o mesmo resultado, mas permanece assíncrono.

Apenas um comentário no seu primeiro código. Está linha

var tasks = await Task.WhenAll(events...

produzirá uma única tarefa para que a variável seja nomeada no singular.

Finalmente, seu último código faz o mesmo, mas é mais sucinto

Para referência: Task.Wait / Task.WhenAll

tede24
fonte
Então o primeiro bloco de código é de fato executado de forma síncrona?
Alexander Derck
1
Sim, porque acessar Result produz uma espera que bloqueia o encadeamento. Por outro lado, quando produz uma nova tarefa, você pode aguardar.
precisa saber é
1
Voltando a esta pergunta e analisando sua observação sobre o nome da tasksvariável, você está completamente certo. Escolha horrível, nem sequer são tarefas, pois são aguardadas imediatamente. Só vou deixar a pergunta como é
Alexander Derck 22/02
13

Com os métodos atuais disponíveis no Linq, parece bastante feio:

var tasks = items.Select(
    async item => new
    {
        Item = item,
        IsValid = await IsValid(item)
    });
var tuples = await Task.WhenAll(tasks);
var validItems = tuples
    .Where(p => p.IsValid)
    .Select(p => p.Item)
    .ToList();

Esperamos que as seguintes versões do .NET apresentem ferramentas mais elegantes para lidar com coleções de tarefas e tarefas de coleções.

Vitaliy Ulantikov
fonte
12

Eu usei este código:

public static async Task<IEnumerable<TResult>> SelectAsync<TSource,TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> method)
{
      return await Task.WhenAll(source.Select(async s => await method(s)));
}

como isso:

var result = await sourceEnumerable.SelectAsync(async s=>await someFunction(s,other params));
Siderite Zackwehdex
fonte
5
Isso só envolve a funcionalidade existente de forma mais obscura imo
Alexander Derck
A alternativa é resultado var = aguardam Task.WhenAll (sourceEnumerable.Select (assíncrono s => aguardam someFunction (s, outros parâmetros)) Ele funciona, também, mas não é LINQy.
Siderite Zackwehdex
Não deve Func<TSource, Task<TResult>> methodconter o other paramsmencionado no segundo bit de código?
matramos 12/09/18
2
Os parâmetros extras são externos, dependendo da função que desejo executar, são irrelevantes no contexto do método de extensão.
Siderite Zackwehdex
4
Esse é um método de extensão adorável. Não sei por que foi considerado "mais obscuro" - é semanticamente análogo ao síncrono Select(), assim como um elegante comparecimento.
usar o seguinte comando
10

Eu prefiro isso como um método de extensão:

public static async Task<IEnumerable<T>> WhenAll<T>(this IEnumerable<Task<T>> tasks)
{
    return await Task.WhenAll(tasks);
}

Para que seja utilizável com o encadeamento de métodos:

var inputs = await events
  .Select(async ev => await ProcessEventAsync(ev))
  .WhenAll()
Daryl
fonte
1
Você não deve chamar o método Waitquando ele não estiver realmente esperando. Está criando uma tarefa que está completa quando todas as tarefas estão completas. Chame-o WhenAll, como o Taskmétodo que ele emula. Também não faz sentido o método ser async. Basta ligar WhenAlle terminar com isso.
Servy
Um pouco de um invólucro inútil na minha opinião, quando ele só chama o método original
Alexander Derck
@ Fair point, mas não gosto particularmente de nenhuma das opções de nome. WhenAll faz parecer um evento que não é bem assim.
Daryl
3
@AlexanderDerck a vantagem é que você pode usá-lo no encadeamento de métodos.
Daryl
1
@Daryl, como WhenAllretorna uma lista avaliada (não é avaliada preguiçosamente), um argumento pode ser usado para usar o Task<T[]>tipo de retorno para significar isso. Quando aguardado, ele ainda poderá usar o Linq, mas também comunicará que não é preguiçoso.
JAD