Async warten in linq select

180

Ich muss ein vorhandenes Programm ändern und es enthält folgenden Code:

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

Aber das scheint mir sehr seltsam, vor allem die Verwendung von asyncund awaitin der Auswahl. Nach dieser Antwort von Stephen Cleary sollte ich diese fallen lassen können.

Dann die zweite, Selectdie das Ergebnis auswählt. Bedeutet dies nicht, dass die Aufgabe überhaupt nicht asynchron ist und synchron ausgeführt wird (so viel Aufwand für nichts), oder wird die Aufgabe asynchron ausgeführt und wenn sie erledigt ist, wird der Rest der Abfrage ausgeführt?

Soll ich den obigen Code wie folgt schreiben, gemäß einer anderen Antwort von Stephen Cleary :

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

und ist es ganz so?

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

Während ich an diesem Projekt arbeite, möchte ich das erste Codebeispiel ändern, aber ich bin nicht besonders daran interessiert, asynchronen Code zu ändern (anscheinend zu funktionieren). Vielleicht mache ich mir nur um nichts Sorgen und alle 3 Codebeispiele machen genau das Gleiche?

ProcessEventsAsync sieht folgendermaßen aus:

async Task<InputResult> ProcessEventAsync(InputEvent ev) {...}
Alexander Derck
quelle
Was ist der Rückgabetyp von ProceesEventAsync?
Tede24
@ tede24 Es ist Task<InputResult>mit InputResulteiner benutzerdefinierten Klasse.
Alexander Derck
Ihre Versionen sind meiner Meinung nach viel einfacher zu lesen. Sie haben jedoch Selectdie Ergebnisse der Aufgaben vor Ihrem vergessen Where.
Max
Und InputResult hat eine Result-Eigenschaft, oder?
Tede24
@ tede24 Ergebnis ist Eigentum der Aufgabe, nicht meiner Klasse. Und @Max das Warten sollte sicherstellen, dass ich die Ergebnisse erhalte, ohne auf das ResultEigentum der Aufgabe zuzugreifen
Alexander Derck

Antworten:

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

Aber das scheint mir sehr seltsam, vor allem die Verwendung von Async und warten in der Auswahl. Nach dieser Antwort von Stephen Cleary sollte ich diese fallen lassen können.

Der Anruf an Selectist gültig. Diese beiden Zeilen sind im Wesentlichen identisch:

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

(Es gibt einen kleinen Unterschied, wie eine synchrone Ausnahme ausgelöst wird ProcessEventAsync, aber im Kontext dieses Codes spielt dies überhaupt keine Rolle.)

Dann die zweite Auswahl, die das Ergebnis auswählt. Bedeutet dies nicht, dass die Aufgabe überhaupt nicht asynchron ist und synchron ausgeführt wird (so viel Aufwand für nichts), oder wird die Aufgabe asynchron ausgeführt und wenn sie erledigt ist, wird der Rest der Abfrage ausgeführt?

Dies bedeutet, dass die Abfrage blockiert wird. Es ist also nicht wirklich asynchron.

Brechen sie ab:

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

startet zuerst eine asynchrone Operation für jedes Ereignis. Dann diese Zeile:

                   .Select(t => t.Result)

wartet darauf, dass diese Vorgänge nacheinander abgeschlossen werden (zuerst wird auf den Vorgang des ersten Ereignisses gewartet, dann auf den nächsten, dann auf den nächsten usw.).

Dies ist der Teil, den ich nicht mag, weil er blockiert und auch Ausnahmen einschließt AggregateException.

und ist es ganz so?

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

Ja, diese beiden Beispiele sind gleichwertig. Beide starten alle asynchronen Operationen ( events.Select(...)), warten dann asynchron, bis alle Operationen in beliebiger Reihenfolge abgeschlossen sind ( await Task.WhenAll(...)), und fahren dann mit dem Rest der Arbeit fort ( Where...).

Beide Beispiele unterscheiden sich vom ursprünglichen Code. Der ursprüngliche Code blockiert und schließt Ausnahmen ein AggregateException.

Stephen Cleary
quelle
Prost, dass du das geklärt hast! Anstelle der in einen eingepackten Ausnahmen AggregateExceptionwürde ich im zweiten Code mehrere separate Ausnahmen erhalten?
Alexander Derck
1
@AlexanderDerck: Nein, sowohl im alten als auch im neuen Code wird nur die erste Ausnahme ausgelöst. Aber Resultdamit wäre eingewickelt AggregateException.
Stephen Cleary
Mit diesem Code wird in meinem ASP.NET MVC-Controller ein Deadlock angezeigt. Ich habe es mit Task.Run (…) gelöst. Ich habe kein gutes Gefühl dabei. Es wurde jedoch genau richtig beendet, als ein asynchroner xUnit-Test ausgeführt wurde. Was ist los?
SuperJMN
2
@ SuperJMN: Ersetzen stuff.Select(x => x.Result);durchawait Task.WhenAll(stuff)
Stephen Cleary
1
@ Daniels: Sie sind im Wesentlichen gleich. Es gibt einige Unterschiede wie Zustandsautomaten, Erfassungskontext und Verhalten synchroner Ausnahmen. Weitere Informationen unter blog.stephencleary.com/2016/12/eliding-async-await.html
Stephen Cleary
25

Vorhandener Code funktioniert, blockiert jedoch den Thread.

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

erstellt für jedes Ereignis eine neue Aufgabe, aber

.Select(t => t.Result)

Blockiert den Thread, der auf das Ende jeder neuen Aufgabe wartet.

Andererseits erzeugt Ihr Code das gleiche Ergebnis, bleibt jedoch asynchron.

Nur ein Kommentar zu Ihrem ersten Code. Diese Linie

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

erzeugt eine einzelne Aufgabe, daher sollte die Variable im Singular benannt werden.

Schließlich macht Ihr letzter Code dasselbe, ist aber prägnanter

Als Referenz: Task.Wait / Task.WhenAll

tede24
quelle
Der erste Codeblock wird also tatsächlich synchron ausgeführt?
Alexander Derck
1
Ja, da der Zugriff auf das Ergebnis eine Wartezeit erzeugt, die den Thread blockiert. Wenn Sie eine neue Aufgabe erstellen, können Sie darauf warten.
Tede24
1
Wenn Sie auf diese Frage zurückkommen und Ihre Bemerkung zum Namen der tasksVariablen betrachten, haben Sie vollkommen recht. Schreckliche Wahl, sie sind nicht einmal Aufgaben, da sie sofort erwartet werden. Ich lasse die Frage einfach so wie sie ist
Alexander Derck
13

Mit den aktuellen in Linq verfügbaren Methoden sieht es ziemlich hässlich aus:

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

Hoffentlich werden die folgenden Versionen von .NET elegantere Tools für die Bearbeitung von Aufgabensammlungen und Aufgaben von Sammlungen bieten.

Vitaliy Ulantikov
quelle
12

Ich habe diesen Code verwendet:

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

so was:

var result = await sourceEnumerable.SelectAsync(async s=>await someFunction(s,other params));
Siderit Zackwehdex
quelle
5
Dies umhüllt nur die vorhandene Funktionalität in einer
dunkeleren
Die Alternative ist var result = warte auf Task.WhenAll (sourceEnumerable.Select (async s => warte auf someFunction (s, andere Parameter)). Es funktioniert auch, aber es ist nicht LINQy
Siderite Zackwehdex
Sollte nicht Func<TSource, Task<TResult>> methoddas other paramsauf dem zweiten Codebit erwähnte enthalten ?
Matramos
2
Die zusätzlichen Parameter sind extern, abhängig von der Funktion, die ich ausführen möchte, und im Kontext der Erweiterungsmethode irrelevant.
Siderite Zackwehdex
4
Das ist eine schöne Erweiterungsmethode. Ich bin mir nicht sicher, warum es als "dunkler" eingestuft wurde - es ist semantisch analog zum Synchron Select(), also ein elegantes Drop-In.
nullPainter
10

Ich bevorzuge dies als Erweiterungsmethode:

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

Damit es mit Methodenverkettung verwendet werden kann:

var inputs = await events
  .Select(async ev => await ProcessEventAsync(ev))
  .WhenAll()
Daryl
quelle
1
Sie sollten die Methode nicht aufrufen, Waitwenn sie nicht tatsächlich wartet. Es wird eine Aufgabe erstellt, die abgeschlossen ist, wenn alle Aufgaben abgeschlossen sind. Nennen Sie es WhenAllwie die TaskMethode, die es emuliert. Es ist auch sinnlos für die Methode zu sein async. Rufen Sie einfach an WhenAllund fertig.
Servy
Ein bisschen ein nutzloser Wrapper meiner Meinung nach, wenn er nur die ursprüngliche Methode
aufruft
@Servy fair point, aber ich mag keine der Namensoptionen besonders. WhenAll lässt es wie ein Ereignis klingen, das es nicht ganz ist.
Daryl
3
@AlexanderDerck der Vorteil ist, dass Sie es in der Methodenverkettung verwenden können.
Daryl
1
@Daryl Da WhenAlleine ausgewertete Liste zurückgegeben wird (sie wird nicht träge ausgewertet), kann argumentiert werden, dass der Task<T[]>Rückgabetyp verwendet wird, um dies anzuzeigen. Wenn dies erwartet wird, kann Linq weiterhin verwendet werden, es wird jedoch auch mitgeteilt, dass es nicht faul ist.
JAD