Async czeka w linq select

180

Muszę zmodyfikować istniejący program i zawiera następujący kod:

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

Ale wydaje mi się to bardzo dziwne, przede wszystkim użycie asynci awaitw selekcji. Zgodnie z tą odpowiedzią Stephena Cleary'ego powinienem móc je odrzucić.

Następnie drugi, Selectktóry wybiera wynik. Czy nie oznacza to, że zadanie nie jest w ogóle asynchroniczne i jest wykonywane synchronicznie (tyle wysiłku na nic), czy też będzie wykonywane asynchronicznie, a po zakończeniu reszta zapytania zostanie wykonana?

Czy powinienem napisać powyższy kod jak poniżej według innej odpowiedzi Stephena Cleary :

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

i czy jest zupełnie tak samo?

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

Kiedy pracuję nad tym projektem, chciałbym zmienić pierwszy przykład kodu, ale nie jestem zbyt chętny do zmiany (najwyraźniej działającego) kodu asynchronicznego. Może po prostu martwię się o nic, a wszystkie 3 próbki kodu robią dokładnie to samo?

ProcessEventsAsync wygląda następująco:

async Task<InputResult> ProcessEventAsync(InputEvent ev) {...}
Alexander Derck
źródło
Jaki jest typ zwrotu ProceesEventAsync?
tede24
@ tede24 Chodzi Task<InputResult>o to, InputResultże jest to klasa niestandardowa.
Alexander Derck,
Wasze wersje są moim zdaniem dużo łatwiejsze do odczytania. Zapomniałeś jednak Selecto wynikach zadań wykonanych wcześniej Where.
Max
A InputResult ma właściwość Result?
tede24
@ tede24 Wynik jest właściwością zadania, a nie mojej klasy. A @Max oczekiwanie powinno upewnić się, że otrzymam wyniki bez dostępu do Resultwłaściwości zadania
Alexander Derck

Odpowiedzi:

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

Ale wydaje mi się to bardzo dziwne, przede wszystkim użycie async i czekanie w selekcji. Zgodnie z tą odpowiedzią Stephena Cleary'ego powinienem móc je odrzucić.

Wezwanie do Selectjest ważne. Te dwie linie są zasadniczo identyczne:

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

(Istnieje niewielka różnica w sposobie wyrzucenia wyjątku synchronicznego ProcessEventAsync, ale w kontekście tego kodu nie ma to żadnego znaczenia).

Następnie drugi Wybierz, który wybiera wynik. Czy to nie oznacza, że ​​zadanie nie jest w ogóle asynchroniczne i jest wykonywane synchronicznie (tyle wysiłku na nic), czy też będzie wykonywane asynchronicznie, a po zakończeniu reszta zapytania zostanie wykonana?

Oznacza to, że zapytanie jest blokowane. Więc to nie jest tak naprawdę asynchroniczne.

Rozbijając to:

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

najpierw rozpocznie operację asynchroniczną dla każdego zdarzenia. Następnie ta linia:

                   .Select(t => t.Result)

będzie czekać, aż te operacje zakończą się pojedynczo (najpierw czeka na operację pierwszego zdarzenia, potem następne, potem następne itd.).

To jest część, na której mi nie zależy, ponieważ blokuje, a także zawija wszelkie wyjątki AggregateException.

i czy jest zupełnie tak samo?

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

Tak, te dwa przykłady są równoważne. Obaj rozpoczynają wszystkie operacje asynchroniczne ( events.Select(...)), następnie asynchronicznie czekają na zakończenie wszystkich operacji w dowolnej kolejności ( await Task.WhenAll(...)), a następnie wykonują resztę pracy ( Where...).

Oba te przykłady różnią się od oryginalnego kodu. Oryginalny kod jest blokujący i zawija wyjątki AggregateException.

Stephen Cleary
źródło
Dzięki za wyjaśnienie tego! Więc zamiast wyjątków opakowanych w jeden AggregateExceptionI otrzymam wiele oddzielnych wyjątków w drugim kodzie?
Alexander Derck,
1
@AlexanderDerck: Nie, zarówno w starym, jak i nowym kodzie zostanie zgłoszony tylko pierwszy wyjątek. Ale z Resulttym byłby zapakowany AggregateException.
Stephen Cleary
Otrzymuję zakleszczenie w moim kontrolerze ASP.NET MVC przy użyciu tego kodu. Rozwiązałem to za pomocą Task.Run (…). Nie mam co do tego dobrego przeczucia. Jednak zakończyło się to dokładnie podczas uruchamiania testu asynchronicznego xUnit. Co się dzieje?
SuperJMN
2
@SuperJMN: Wymień stuff.Select(x => x.Result);zawait Task.WhenAll(stuff)
Stephen Cleary
1
@DanielS: Zasadniczo są takie same. Istnieją pewne różnice, takie jak automaty stanowe, kontekst przechwytywania, zachowanie wyjątków synchronicznych. Więcej informacji na blog.stephencleary.com/2016/12/eliding-async-await.html
Stephen Cleary
25

Istniejący kod działa, ale blokuje wątek.

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

tworzy nowe zadanie dla każdego wydarzenia, ale

.Select(t => t.Result)

blokuje wątek czekając na zakończenie każdego nowego zadania.

Z drugiej strony twój kod daje ten sam wynik, ale zachowuje asynchroniczność.

Tylko jeden komentarz do Twojego pierwszego kodu. Ta linia

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

utworzy jedno zadanie, więc zmienna powinna być nazwana w liczbie pojedynczej.

Wreszcie twój ostatni kod robi to samo, ale jest bardziej zwięzły

Dla odniesienia: Task.Wait / Task.WhenAll

tede24
źródło
Czyli pierwszy blok kodu jest faktycznie wykonywany synchronicznie?
Alexander Derck,
1
Tak, ponieważ dostęp do wyniku generuje oczekiwanie, które blokuje wątek. Z drugiej strony Kiedy tworzy nowe zadanie, na które możesz czekać.
tede24
1
Wracając do tego pytania i patrząc na Twoją uwagę dotyczącą nazwy taskszmiennej, masz całkowitą rację. Okropny wybór, nie są to nawet zadania, ponieważ są od razu oczekiwane. Po prostu zostawię pytanie tak, jak jest
Alexander Derck
13

Przy obecnych metodach dostępnych w Linq wygląda to dość brzydko:

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

Miejmy nadzieję, że kolejne wersje .NET dostarczą bardziej eleganckiego narzędzia do obsługi zbiorów zadań i zadań związanych z kolekcjami.

Vitaliy Ulantikov
źródło
12

Użyłem tego kodu:

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

lubię to:

var result = await sourceEnumerable.SelectAsync(async s=>await someFunction(s,other params));
Siderite Zackwehdex
źródło
5
To po prostu otacza istniejącą funkcjonalność w bardziej niejasny sposób.
Imo
Alternatywą jest var result = await Task.WhenAll (sourceEnumerable.Select (async s => await someFunction (s, other params)). To też działa, ale to nie jest LINQy
Siderite Zackwehdex
Czy nie powinien Func<TSource, Task<TResult>> methodzawierać other paramswspomnianego w drugim bicie kodu?
matramos
2
Dodatkowe parametry są zewnętrzne, w zależności od funkcji, którą chcę wykonać, nie mają znaczenia w kontekście metody rozszerzenia.
Siderite Zackwehdex
4
To urocza metoda przedłużania. Nie wiem, dlaczego uznano to za „bardziej niejasne” - jest semantycznie analogiczne do synchronicznego Select(), więc jest to eleganckie drop-in.
nullPainter
11

Wolę to jako metodę rozszerzenia:

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

Aby można go było używać z łańcuchem metod:

var inputs = await events
  .Select(async ev => await ProcessEventAsync(ev))
  .WhenAll()
Daryl
źródło
1
Nie powinieneś wywoływać metody, Waitgdy w rzeczywistości nie czeka. Tworzy zadanie, które jest zakończone, gdy wszystkie zadania są ukończone. Nazwij to WhenAlltak, jak Taskmetoda, którą naśladuje. Nie ma też sensu, aby ta metoda była async. Po prostu zadzwoń WhenAlli skończ z tym.
Servy
Trochę bezużyteczne opakowanie, moim zdaniem, gdy nazywa się po prostu oryginalną metodą
Alexander Derck,
@Servy uczciwa uwaga, ale nie podoba mi się żadna z opcji nazw. WhenAll sprawia, że ​​brzmi to jak wydarzenie, którym nie jest do końca.
Daryl
3
@AlexanderDerck zaletą jest to, że można go używać w łańcuchu metod.
Daryl
1
@Daryl, ponieważ WhenAllzwraca listę ocenioną (nie jest oceniana leniwie), można ustawić argument, aby używał Task<T[]>zwracanego typu, aby to oznaczyć. Oczekiwany, nadal będzie mógł korzystać z Linq, ale również informuje, że nie jest leniwy.
JAD