Jak oczekiwać na listę zadań asynchronicznie przy użyciu LINQ?

87

Mam listę zadań, które utworzyłem w ten sposób:

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

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

    ...
}

Po użyciu .ToList()wszystkie zadania powinny się rozpocząć. Teraz chcę poczekać na ich zakończenie i zwrócić wyniki.

Działa to w powyższym ...bloku:

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

Robi, co chcę, ale wydaje się to raczej niezdarne. Wolałbym napisać coś prostszego:

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

... ale to się nie kompiluje. czego mi brakuje? A może po prostu nie można tego wyrazić w ten sposób?

Matt Johnson-Pint
źródło
Czy musisz przetwarzać DoSomethingAsync(foo)kolejno dla każdego foo, czy jest to kandydat do Parallel.ForEach <Foo> ?
mdisibio,
1
@mdisibio - Parallel.ForEachblokuje. Wzorzec tutaj pochodzi z asynchronicznego wideo C # Jona Skeeta w witrynie Pluralsight . Wykonuje się równolegle bez blokowania.
Matt Johnson-Pint
@mdisibio - Nope. Biegną równolegle. Spróbuj . (Dodatkowo wygląda na to, że nie potrzebuję, .ToList()jeśli mam zamiar użyć WhenAll.)
Matt Johnson-Pint,
Punkt zajęty. W zależności od tego, jak DoSomethingAsyncjest napisana, lista może być wykonywana równolegle lub nie. Udało mi się napisać metodę testową, która była i wersję, której nie było, ale w każdym przypadku zachowanie jest podyktowane samą metodą, a nie delegatem tworzącym zadanie. Przepraszam za zamieszanie. Jeśli jednak DoSomethingAsycwróci Task<Foo>, to awaitobecność delegata nie jest absolutnie konieczna ... Myślę, że to był główny punkt, który zamierzałem zrobić.
mdisibio

Odpowiedzi:

136

LINQ nie działa idealnie z asynckodem, ale możesz to zrobić:

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

Jeśli wszystkie zadania zwracają ten sam typ wartości, możesz nawet zrobić to:

var results = await Task.WhenAll(tasks);

co jest całkiem miłe. WhenAllzwraca tablicę, więc uważam, że twoja metoda może zwrócić wyniki bezpośrednio:

return await Task.WhenAll(tasks);
Stephen Cleary
źródło
11
Chciałem tylko zaznaczyć, że to również może działaćvar tasks = foos.Select(foo => DoSomethingAsync(foo)).ToList();
mdisibio,
1
lub nawetvar tasks = foos.Select(DoSomethingAsync).ToList();
Todd Menier
3
Jaki jest powód tego, że Linq nie działa idealnie z kodem asynchronicznym?
Ehsan Sajjad
2
@EhsanSajjad: Ponieważ LINQ to Objects synchronicznie działa na obiektach w pamięci. Niektóre ograniczone rzeczy działają, na przykład Select. Ale większość nie lubi Where.
Stephen Cleary
4
@EhsanSajjad: Jeśli operacja jest oparta na we / wy, możesz użyć asyncdo zredukowania wątków; jeśli jest związany z procesorem i znajduje się już w wątku w tle, asyncnie przyniesie żadnych korzyści.
Stephen Cleary
9

Aby rozwinąć odpowiedź Stephena, stworzyłem następującą metodę rozszerzenia, aby zachować płynny styl LINQ. Możesz to zrobić

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);
        }
    }
}
Łaskawy
źródło
10
Osobiście nazwałbym twoją metodę przedłużaniaToArrayAsync
torvin
4

Jednym z problemów z Task.WhenAll jest to, że stworzyłoby to równoległość. W większości przypadków może być nawet lepiej, ale czasami chcesz tego uniknąć. Na przykład odczytywanie danych partiami z bazy danych i wysyłanie danych do niektórych zdalnych usług internetowych. Nie chcesz ładować wszystkich partii do pamięci, ale trafisz do bazy danych po przetworzeniu poprzedniej partii. Więc musisz przełamać asynchroniczność. Oto przykład:

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

Uwaga .GetAwaiter (). GetResult () konwertuje go na synchronizację. DB zostałby uderzony leniwie tylko po przetworzeniu zdarzeń batchSize.

Boris Lipschitz
źródło
1

Użyj Task.WaitAlllub Task.WhenAllcokolwiek jest odpowiednie.

FUNT
źródło
1
To też nie działa. Task.WaitAllblokuje, nie jest oczekiwany i nie będzie działać z plikiem Task<T>.
Matt Johnson-Pint
@MattJohnson WhenAll?
LB
Tak. Otóż ​​to! Czuję się głupio. Dzięki!
Matt Johnson-Pint
0

Zadanie Kiedy wszyscy powinni tutaj załatwić sprawę.

Ameen
źródło