Async aguarda en linq select

180

Necesito modificar un programa existente y contiene el siguiente código:

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

Pero esto me parece muy extraño, en primer lugar, el uso de asyncy awaiten la selección. De acuerdo con esta respuesta por Stephen Cleary que debe ser capaz de soltar esos.

Luego el segundo Selectque selecciona el resultado. ¿No significa esto que la tarea no es asíncrona en absoluto y se realiza de forma sincrónica (tanto esfuerzo para nada), o la tarea se realizará de forma asincrónica y cuando se realiza el resto de la consulta se ejecuta?

¿Debo escribir el código anterior como el siguiente según otra respuesta de Stephen Cleary :

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

y es completamente igual a esto?

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

Mientras estoy trabajando en este proyecto, me gustaría cambiar el primer ejemplo de código, pero no estoy demasiado interesado en cambiar (aparentemente trabajando) el código asíncrono. ¿Tal vez solo me estoy preocupando por nada y las 3 muestras de código hacen exactamente lo mismo?

ProcessEventsAsync tiene este aspecto:

async Task<InputResult> ProcessEventAsync(InputEvent ev) {...}
Alexander Derck
fuente
¿Cuál es el tipo de devolución de ProceesEventAsync?
tede24
@ tede24 Es Task<InputResult>con InputResultser una clase personalizada.
Alexander Derck
Sus versiones son mucho más fáciles de leer en mi opinión. Sin embargo, ha olvidado Selectlos resultados de las tareas anteriores a su Where.
Max
¿Y InputResult tiene una propiedad Result derecha?
tede24
@ tede24 El resultado es propiedad de la tarea, no de mi clase. Y @Max the wait debería asegurarse de obtener los resultados sin acceder a la Resultpropiedad de la tarea
Alexander Derck

Respuestas:

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

Pero esto me parece muy extraño, en primer lugar, el uso de async y aguardar en la selección. De acuerdo con esta respuesta de Stephen Cleary, debería poder dejarlos.

La llamada a Selectes válida. Estas dos líneas son esencialmente idénticas:

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

(Hay una pequeña diferencia con respecto a cómo se generaría una excepción síncrona ProcessEventAsync, pero en el contexto de este código no importa en absoluto).

Luego, el segundo Seleccionar que selecciona el resultado. ¿No significa esto que la tarea no es asíncrona en absoluto y se realiza de forma sincrónica (tanto esfuerzo para nada), o la tarea se realizará de forma asincrónica y cuando se realiza el resto de la consulta se ejecuta?

Significa que la consulta está bloqueando. Por lo tanto, no es realmente asíncrono.

Desglosándolo:

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

primero comenzará una operación asincrónica para cada evento. Entonces esta línea:

                   .Select(t => t.Result)

esperará a que esas operaciones se completen una a la vez (primero espera la operación del primer evento, luego la siguiente, luego la siguiente, etc.).

Esta es la parte que no me importa, porque bloquea y también incluye cualquier excepción AggregateException.

y es completamente igual a esto?

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

Sí, esos dos ejemplos son equivalentes. Ambos comienzan todas las operaciones asincrónicas ( events.Select(...)), luego esperan asincrónicamente a que se completen todas las operaciones en cualquier orden ( await Task.WhenAll(...)), luego continúan con el resto del trabajo (Where... ).

Ambos ejemplos son diferentes del código original. El código original está bloqueando y envolverá las excepciones AggregateException.

Stephen Cleary
fuente
¡Saludos por aclarar eso! Entonces, en lugar de las excepciones envueltas en un AggregateException, ¿obtendría múltiples excepciones separadas en el segundo código?
Alexander Derck
1
@AlexanderDerck: No, tanto en el código antiguo como en el nuevo, solo se generaría la primera excepción. Pero con Resulteso estaría envuelto AggregateException.
Stephen Cleary
Estoy obteniendo un punto muerto en mi controlador ASP.NET MVC usando este código. Lo resolví usando Task.Run (...). No tengo un buen presentimiento al respecto. Sin embargo, terminó justo cuando se encontró con una prueba xUnit asíncrona. ¿Que esta pasando?
SuperJMN
2
@SuperJMN: Reemplazar stuff.Select(x => x.Result);conawait Task.WhenAll(stuff)
Stephen Cleary
1
@DanielS: Son esencialmente lo mismo. Existen algunas diferencias, como máquinas de estado, captura de contexto, comportamiento de excepciones síncronas. Más información en blog.stephencleary.com/2016/12/eliding-async-await.html
Stephen Cleary
25

El código existente funciona, pero bloquea el hilo.

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

crea una nueva tarea para cada evento, pero

.Select(t => t.Result)

bloquea el hilo esperando que finalice cada nueva tarea.

Por otro lado, su código produce el mismo resultado pero se mantiene asíncrono.

Solo un comentario en tu primer código. Esta línea

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

producirá una sola tarea, por lo que la variable debe nombrarse en singular.

Finalmente su último código hace lo mismo pero es más sucinto

Para referencia: Task.Wait / Task.WhenAll

tede24
fuente
Entonces, ¿el primer bloque de código se ejecuta de forma síncrona?
Alexander Derck
1
Sí, porque acceder a Result produce un Wait que bloquea el hilo. Por otro lado, cuando produce una nueva tarea que puede esperar.
tede24
1
Volviendo a esta pregunta y mirando su comentario sobre el nombre de la tasksvariable, tiene toda la razón. Elección horrible, ni siquiera son tareas, ya que se les espera de inmediato. Sin embargo
Alexander Derck
13

Con los métodos actuales disponibles en Linq, se ve bastante feo:

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

Esperemos que las siguientes versiones de .NET presenten herramientas más elegantes para manejar colecciones de tareas y tareas de colecciones.

Vitaliy Ulantikov
fuente
12

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

Me gusta esto:

var result = await sourceEnumerable.SelectAsync(async s=>await someFunction(s,other params));
Siderite Zackwehdex
fuente
55
Esto simplemente envuelve la funcionalidad existente de una manera más oscura imo
Alexander Derck
La alternativa es var result = await Task.WhenAll (sourceEnumerable.Select (async s => await someFunction (s, other params)). También funciona, pero no es LINQy
Siderite Zackwehdex
¿No debería Func<TSource, Task<TResult>> methodcontener lo other paramsmencionado en el segundo bit de código?
matramos el
2
Los parámetros adicionales son externos, dependiendo de la función que quiero ejecutar, son irrelevantes en el contexto del método de extensión.
Siderite Zackwehdex
55
Ese es un método de extensión encantador. No estoy seguro de por qué se consideró "más oscuro": es semánticamente análogo a lo síncrono Select(), por lo que es un complemento elegante.
nullPainter
11

Prefiero esto como un método de extensión:

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

Para que se pueda usar con el método de encadenamiento:

var inputs = await events
  .Select(async ev => await ProcessEventAsync(ev))
  .WhenAll()
Daryl
fuente
1
No debe llamar al método Waitcuando en realidad no está esperando. Está creando una tarea que se completa cuando se completan todas las tareas. Llámalo WhenAll, como el Taskmétodo que emula. Tampoco tiene sentido que el método sea async. Simplemente llame WhenAlly termine con eso.
Servy
En mi opinión, es una
especie de
@Servy fair point, pero no me gustan particularmente las opciones de nombre. WhenAll hace que parezca un evento que no es del todo.
Daryl
3
@AlexanderDerck la ventaja es que puedes usarlo en el método de encadenamiento.
Daryl
1
@Daryl porque WhenAlldevuelve una lista evaluada (no se evalúa perezosamente), se puede hacer un argumento para usar el Task<T[]>tipo de devolución para significar eso. Cuando se espera, esto aún podrá usar Linq, pero también comunica que no es perezoso.
JAD