Async attende in linq select


180

Devo modificare un programma esistente e contiene il seguente codice:

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

Ma questo mi sembra molto strano, prima di tutto l'uso di asynce awaitnella selezione. Secondo questa risposta di Stephen Cleary dovrei riuscire a lasciar perdere.

Quindi il secondo Selectche seleziona il risultato. Questo non significa che l'attività non è affatto asincrona e viene eseguita in modo sincrono (così tanto sforzo per nulla), oppure l'attività verrà eseguita in modo asincrono e al termine verrà eseguita la parte restante della query?

Dovrei scrivere il codice sopra come segue secondo un'altra risposta di Stephen Cleary :

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

ed è completamente uguale a questo?

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

Mentre sto lavorando a questo progetto mi piacerebbe cambiare il primo esempio di codice ma non sono troppo entusiasta di cambiare il codice asincrono (apparentemente funzionante). Forse mi sto solo preoccupando per niente e tutti e 3 gli esempi di codice fanno esattamente la stessa cosa?

ProcessEventsAsync è simile al seguente:

async Task<InputResult> ProcessEventAsync(InputEvent ev) {...}

Qual è il tipo di restituzione di ProceesEventAsync?
tede24,

@ tede24 E ' Task<InputResult>con InputResultessendo una classe personalizzata.
Alexander Derck,

Le tue versioni sono molto più facili da leggere secondo me. Tuttavia, hai dimenticato Selecti risultati delle attività precedenti al tuo Where.
Max

E InputResult ha una proprietà Result, giusto?
tede24,

@ tede24 Il risultato è proprietà dell'attività non della mia classe. E @Max the waitit dovrebbe assicurarsi di ottenere i risultati senza accedere alla Resultproprietà dell'attività
Alexander Derck,

Risposte:


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

Ma questo mi sembra molto strano, prima di tutto l'uso di asincrono e attendere nella selezione. Secondo questa risposta di Stephen Cleary dovrei riuscire a lasciar perdere.

La chiamata a Selectè valida. Queste due linee sono sostanzialmente identiche:

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

(C'è una piccola differenza riguardo al modo in cui verrebbe generata un'eccezione sincrona ProcessEventAsync, ma nel contesto di questo codice non importa affatto.)

Quindi il secondo Seleziona che seleziona il risultato. Questo non significa che l'attività non è affatto asincrona e viene eseguita in modo sincrono (così tanto sforzo per nulla), oppure l'attività verrà eseguita in modo asincrono e al termine verrà eseguita la parte restante della query?

Significa che la query sta bloccando. Quindi non è davvero asincrono.

Abbattendo:

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

avvia prima un'operazione asincrona per ciascun evento. Quindi questa linea:

                   .Select(t => t.Result)

attenderà il completamento di tali operazioni una alla volta (prima attende l'operazione del primo evento, quindi il successivo, quindi il successivo, ecc.).

Questa è la parte che non mi interessa, perché blocca e avvolgerebbe anche qualsiasi eccezione AggregateException.

ed è completamente uguale a questo?

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ì, questi due esempi sono equivalenti. Entrambi avviano tutte le operazioni asincrone ( events.Select(...)), quindi attendono in modo asincrono il completamento di tutte le operazioni in qualsiasi ordine ( await Task.WhenAll(...)), quindi procedono con il resto del lavoro ( Where...).

Entrambi questi esempi sono diversi dal codice originale. Il codice originale sta bloccando e includerà le eccezioni AggregateException.


Saluti per averlo chiarito! Quindi, invece delle eccezioni racchiuse in un AggregateException, otterrei più eccezioni separate nel secondo codice?
Alexander Derck,

1
@AlexanderDerck: No, sia nel vecchio che nel nuovo codice, verrebbe sollevata solo la prima eccezione. Ma con Resultesso sarebbe avvolto AggregateException.
Stephen Cleary,

Ricevo un deadlock nel mio controller MVC ASP.NET usando questo codice. L'ho risolto usando Task.Run (...). Non ho una buona sensazione al riguardo. Tuttavia, è terminato perfettamente quando si esegue un test xUnit asincrono. Cosa sta succedendo?
SuperJMN

2
@SuperJMN: Sostituisci stuff.Select(x => x.Result);conawait Task.WhenAll(stuff)
Stephen Cleary,

1
@DanielS: sono essenzialmente gli stessi. Vi sono alcune differenze come macchine a stati, contesto di acquisizione, comportamento delle eccezioni sincrone. Maggiori informazioni su blog.stephencleary.com/2016/12/eliding-async-await.html
Stephen Cleary,

25

Il codice esistente funziona, ma sta bloccando il thread.

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

crea una nuova attività per ogni evento, ma

.Select(t => t.Result)

blocca il thread in attesa che ogni nuova attività termini.

D'altra parte il tuo codice produce lo stesso risultato ma rimane asincrono.

Solo un commento sul tuo primo codice. Questa linea

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

produrrà una singola attività, quindi la variabile dovrebbe essere nominata in singolare.

Finalmente il tuo ultimo codice fa lo stesso ma è più succinto

Per riferimento: Task.Wait / Task.WhenAll


Quindi il primo blocco di codice viene effettivamente eseguito in modo sincrono?
Alexander Derck,

1
Sì, perché l'accesso a Result produce un'attesa che blocca il thread. D'altra parte, quando produce una nuova attività che puoi aspettare.
tede24,

1
Tornando a questa domanda e guardando la tua osservazione sul nome della tasksvariabile, hai perfettamente ragione. Scelta orribile, non sono nemmeno compiti mentre vengono attesi subito. Lascerò la domanda come se fosse
Alexander Derck il

13

Con gli attuali metodi disponibili in Linq sembra abbastanza brutto:

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

Si spera che le seguenti versioni di .NET forniranno strumenti più eleganti per gestire raccolte di attività e attività di raccolte.


12

Ho usato questo codice:

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

come questo:

var result = await sourceEnumerable.SelectAsync(async s=>await someFunction(s,other params));

5
Questo avvolge la funzionalità esistente in un modo più oscuro imo
Alexander Derck,

L'alternativa è var result = await Task.WhenAll (sourceEnumerable.Select (async s => waitit someFunction (s, altri parametri)). Funziona anche, ma non è LINQy
Zackwehdex

Non dovrebbe Func<TSource, Task<TResult>> methodcontenere il other paramsmenzionato sul secondo bit di codice?
matramos,

2
I parametri extra sono esterni, a seconda della funzione che voglio eseguire, sono irrilevanti nel contesto del metodo di estensione.
Siderite Zackwehdex,

5
Questo è un metodo di estensione delizioso. Non so perché sia ​​stato considerato "più oscuro" - è semanticamente analogo al sincrono Select(), quindi è un elegante drop-in.
nullPainter

11

Preferisco questo come metodo di estensione:

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

In modo che sia utilizzabile con il concatenamento del metodo:

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

1
Non dovresti chiamare il metodo Waitquando in realtà non sta aspettando. Sta creando un'attività che è completa quando tutte le attività sono complete. Chiamalo WhenAll, come il Taskmetodo che emula. È anche inutile che il metodo sia async. Basta chiamare WhenAlled essere finito.
Serve

Un po 'di un involucro inutile secondo me quando chiama semplicemente il metodo originale
Alexander Derck

@Servy punto giusto, ma non mi piace particolarmente nessuna delle opzioni di nome. WhenAll lo fa sembrare un evento che non è del tutto.
Daryl,

3
@AlexanderDerck il vantaggio è che puoi usarlo nel concatenamento di metodi.
Daryl,

1
@Daryl poiché WhenAllrestituisce un elenco valutato (non valutato pigramente), è possibile utilizzare un argomento per utilizzare il Task<T[]>tipo restituito. Se atteso, sarà comunque in grado di utilizzare Linq, ma comunica anche che non è pigro.
JAD,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.