Asynchroniczne oczekiwanie w LINQ select

Muszę zmodyfikować istniejący program i zawiera on 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 async i await w select. Według tej odpowiedzi Stephena Cleary ' ego powinienem być w stanie je rzucić.

Następnie druga Select, która wybiera wynik. Czy to nie znaczy, że zadanie nie jest w ogóle asynchroniczne i jest wykonywane synchronicznie( tyle wysiłku za nic), czy też zadanie będzie wykonywane asynchronicznie a kiedy to się skończy, reszta zapytań zostanie wykonana?

Czy powinienem napisać powyższy kod zgodnie z kolejna odpowiedź Stephena Cleary ' ego :

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

Podczas pracy nad tym projektem chciałbym zmienić pierwszą próbkę kodu, ale nie jestem zbyt chętny do zmiany (pozornie pracującego) kodu asynchronicznego. Może po prostu martwię się o nic i wszystkie 3 próbki kodu robią dokładnie to samo coś?

ProcessEventsAsync wygląda tak:

async Task<InputResult> ProcessEventAsync(InputEvent ev) {...}
Author: Community, 2016-01-26

7 answers

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

Ale to wydaje mi się bardzo dziwne, przede wszystkim korzystanie z async i czekać w select. Według tej odpowiedzi Stephena Cleary ' ego powinienem być w stanie je rzucić.

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

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

(istnieje niewielka różnica co do tego, jak synchroniczny wyjątek byłby wyrzucany z ProcessEventAsync, ale w kontekście tego kodu nie ma to żadnego znaczenia.)

Następnie drugi Wybierz który wybiera wynik. Czy to nie znaczy, że zadanie nie jest w ogóle asynchroniczne i jest wykonywane synchronicznie (tyle wysiłku za nic), czy też zadanie zostanie wykonane asynchronicznie i kiedy to się skończy, reszta zapytania zostanie wykonana?

Oznacza to, że zapytanie blokuje się. Nie jest więc tak naprawdę asynchroniczny.

Rozbicie:

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

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

                   .Select(t => t.Result)

Będzie czekać na tych operacje do wykonania po kolei (najpierw czeka na operację pierwszego zdarzenia, potem następne, potem następne, itd.).

Jest to część, której nie obchodzi, ponieważ blokuje, a także owija wszelkie wyjątki w 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. Oba uruchamiają wszystkie operacje asynchroniczne (events.Select(...)), następnie asynchronicznie oczekują na zakończenie wszystkich operacji w dowolnej kolejności (await Task.WhenAll(...)), następnie kontynuuj resztę pracy (Where...).

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

 227
Author: Stephen Cleary,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-26 15:11:55

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

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

Tworzy nowe zadanie dla każdego zdarzenia, ale

.Select(t => t.Result)

Blokuje wątek czekający 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. This line

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

Wytworzy pojedyncze zadanie, więc zmienna powinna być nazwana w liczbie pojedynczej.

W końcu twój ostatni kod robi to samo, ale jest bardziej zwięzły

Dla odniesienia: zadanie.Czekaj. / Zadanie.WhenAll

 27
Author: tede24,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-23 10:51:20

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

TAK:

var result = await sourceEnumerable.SelectAsync(async s=>await someFunction(s,other params));
 20
Author: Siderite Zackwehdex,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-22 09:53:04

Wolę to jako metodę rozszerzenia:

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

Tak, że jest użyteczna przy łańcuchowaniu metodą:

var inputs = await events
  .Select(async ev => await ProcessEventAsync(ev))
  .WhenAll()
 17
Author: Daryl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-11 16:13:41

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

Mam nadzieję, że kolejne wersje. NET wymyślą bardziej eleganckie narzędzia do obsługi zbiorów zadań i zadań kolekcji.

 16
Author: Vitaliy Ulantikov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-10 15:43:06

Chciałem zadzwonić Select(...), ale upewnić się, że działa w kolejności, ponieważ równoległe działanie powodowałoby inne problemy z współbieżnością, więc skończyło się na tym. Nie mogę wywołać .Result, ponieważ zablokuje to wątek interfejsu użytkownika.

public static class TaskExtensions
{
    public static async Task<IEnumerable<TResult>> SelectInSequenceAsync<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> asyncSelector)
    {
        var result = new List<TResult>();
        foreach (var s in source)
        {
            result.Add(await asyncSelector(s));
        }
        
        return result;
    }
}

Użycie:

var inputs = events.SelectInSequenceAsync(ev => ProcessEventAsync(ev))
                   .Where(i => i != null)
                   .ToList();
Wiem, że to zadanie./ Align = "center" bgcolor = "# e0ffe0 " / cesarz chin / / align = center /
 2
Author: KTCheek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-10-15 01:35:32

"to, że możesz, nie znaczy, że powinieneś."

Prawdopodobnie możesz używać async / wait w wyrażeniach LINQ tak, aby zachowywały się dokładnie tak, jak chcesz, ale czy jakikolwiek inny programista czytający Twój kod nadal zrozumie jego zachowanie i intencję?

(w szczególności: czy operacje asynchroniczne powinny być uruchamiane równolegle, czy są celowo sekwencyjne? Czy oryginalny deweloper w ogóle o tym pomyślał?)

Widać to również wyraźnie w pytaniu, które wydaje się być zapytany przez programistę próbującego zrozumieć cudzy kod, nie znając jego intencji. Aby upewnić się, że to się nie powtórzy, najlepiej jest przepisać wyrażenie LINQ jako instrukcję pętli, jeśli to możliwe.

 -1
Author: Florian Winter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-08-10 09:16:11