Wie kann ich mit LINQ asynchron auf eine Liste von Aufgaben warten?

87

Ich habe eine Liste von Aufgaben, die ich wie folgt erstellt habe:

public async Task<IList<Foo>> GetFoosAndDoSomethingAsync()
{
    var foos = await GetFoosAsync();

    var tasks = foos.Select(async foo => await DoSomethingAsync(foo)).ToList();

    ...
}

Durch die Verwendung .ToList()sollten alle Aufgaben beginnen. Jetzt möchte ich auf ihre Fertigstellung warten und die Ergebnisse zurückgeben.

Dies funktioniert im obigen ...Block:

var list = new List<Foo>();
foreach (var task in tasks)
    list.Add(await task);
return list;

Es macht was ich will, aber das scheint ziemlich ungeschickt. Ich würde viel lieber so etwas Einfacheres schreiben:

return tasks.Select(async task => await task).ToList();

... aber das wird nicht kompiliert. Was vermisse ich? Oder ist es einfach nicht möglich, Dinge so auszudrücken?

Matt Johnson-Pint
quelle
Müssen Sie DoSomethingAsync(foo)für jedes Foo seriell verarbeiten , oder ist dies ein Kandidat für Parallel.ForEach <Foo> ?
mdisibio
1
@mdisibio - Parallel.ForEachblockiert. Das Muster hier stammt aus Jon Skeets asynchronem C # -Video auf Pluralsight . Es wird parallel ausgeführt, ohne zu blockieren.
Matt Johnson-Pint
@mdisibio - Nein. Sie laufen parallel. Probieren Sie es aus . (Außerdem sieht es so aus, als würde ich es nicht brauchen, .ToList()wenn ich es nur benutzen werde WhenAll.)
Matt Johnson-Pint
Punkt genommen. Je nachdem, wie DoSomethingAsyncgeschrieben wird, wird die Liste möglicherweise parallel ausgeführt oder nicht. Ich konnte eine Testmethode schreiben, die es war, und eine Version, die es nicht war, aber in beiden Fällen wird das Verhalten von der Methode selbst bestimmt, nicht vom Delegaten, der die Aufgabe erstellt. Entschuldigung für die Verwechslung. Wenn Sie jedoch DoSomethingAsyczurückkehren Task<Foo>, ist das awaitim Delegierten nicht unbedingt erforderlich ... Ich denke, das war der Hauptpunkt, den ich versuchen wollte.
mdisibio

Antworten:

134

LINQ funktioniert nicht perfekt mit asyncCode, aber Sie können dies tun:

var tasks = foos.Select(DoSomethingAsync).ToList();
await Task.WhenAll(tasks);

Wenn Ihre Aufgaben alle denselben Werttyp zurückgeben, können Sie sogar Folgendes tun:

var results = await Task.WhenAll(tasks);

das ist ganz nett. WhenAllGibt ein Array zurück, daher glaube ich, dass Ihre Methode die Ergebnisse direkt zurückgeben kann:

return await Task.WhenAll(tasks);
Stephen Cleary
quelle
11
var tasks = foos.Select(foo => DoSomethingAsync(foo)).ToList();
Ich
1
oder sogarvar tasks = foos.Select(DoSomethingAsync).ToList();
Todd Menier
3
Was ist der Grund dafür, dass Linq mit asynchronem Code nicht perfekt funktioniert?
Ehsan Sajjad
2
@EhsanSajjad: Weil LINQ to Objects synchron auf speicherinternen Objekten funktioniert. Einige begrenzte Dinge funktionieren wie Select. Aber die meisten mögen es nicht Where.
Stephen Cleary
4
@EhsanSajjad: Wenn die Operation E / A-basiert ist, können Sie asyncThreads reduzieren. Wenn es CPU-gebunden ist und sich bereits in einem Hintergrund-Thread befindet, asyncbietet es keinen Vorteil.
Stephen Cleary
9

Um Stephens Antwort zu erweitern, habe ich die folgende Erweiterungsmethode erstellt, um den fließenden Stil von LINQ beizubehalten. Sie können dann tun

await someTasks.WhenAll()

namespace System.Linq
{
    public static class IEnumerableExtensions
    {
        public static Task<T[]> WhenAll<T>(this IEnumerable<Task<T>> source)
        {
            return Task.WhenAll(source);
        }
    }
}
Clement
quelle
10
Persönlich würde ich Ihre Erweiterungsmethode nennenToArrayAsync
Torvin
3

Ein Problem mit Task.WhenAll ist, dass dadurch eine Parallelität erzeugt wird. In den meisten Fällen ist es vielleicht sogar noch besser, aber manchmal möchten Sie es vermeiden. Beispiel: Lesen von Daten in Stapeln aus der Datenbank und Senden von Daten an einen Remote-Webdienst. Sie möchten nicht alle Stapel in den Speicher laden, sondern die Datenbank aufrufen, sobald der vorherige Stapel verarbeitet wurde. Sie müssen also die Asynchronität aufheben. Hier ist ein Beispiel:

var events = Enumerable.Range(0, totalCount/ batchSize)
   .Select(x => x*batchSize)
   .Select(x => dbRepository.GetEventsBatch(x, batchSize).GetAwaiter().GetResult())
   .SelectMany(x => x);
foreach (var carEvent in events)
{
}

Hinweis .GetAwaiter (). GetResult () konvertiert es in Synchronose. DB würde nur dann faul getroffen, wenn BatchSize von Ereignissen verarbeitet wurden.

Boris Lipschitz
quelle
1

Verwenden Sie Task.WaitAlloder Task.WhenAllwas auch immer angemessen ist.

PFUND
quelle
1
Das funktioniert auch nicht. Task.WaitAllblockiert, ist nicht zu erwarten und funktioniert nicht mit a Task<T>.
Matt Johnson-Pint
@ MattJohnson WhenAll?
LB
Ja. Das ist es! Ich fühle mich dumm. Vielen Dank!
Matt Johnson-Pint
0

Task.WhenAll sollte hier den Trick machen.

Ameen
quelle