Асинхронізація чекає у вибору linq


180

Мені потрібно змінити існуючу програму, яка містить такий код:

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

Але це здається мені дуже дивним, в першу чергу використанням asyncі awaitу виборі. Відповідно до цієї відповіді Стівена Клірі, я мав би змогу їх відмовити.

Потім друга, Selectяка вибирає результат. Чи це не означає, що завдання зовсім не асинхронізується і виконується синхронно (стільки зусиль ні за що), чи завдання буде виконано асинхронно, і коли воно виконано, решта запиту буде виконана?

Чи повинен я написати вищевказаний код, як слід, відповідно до іншої відповіді Стівена Клірі :

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

Хоча я працюю над цим проектом, я хотів би змінити перший зразок коду, але я не надто прагну змінити (помітно працюючи) код асинхронізації. Можливо, я просто переживаю за все, і всі 3 зразки коду роблять точно те саме?

ProcessEventsAsync виглядає так:

async Task<InputResult> ProcessEventAsync(InputEvent ev) {...}

Який тип повернення ProceesEventAsync?
tede24

@ tede24 Це Task<InputResult>з InputResultтим, щоб бути користувацьким класом.
Олександр Дерк

На мою думку, ваші версії набагато легше читати. Однак ви забули Selectрезультати своїх завдань перед своїми Where.
Макс

І InputResult має право власності Result?
tede24

@ tede24 Результат є властивістю завдання, а не мого класу. І @Max очікуючий повинен переконатися, що я отримаю результати, не маючи доступу до Resultвластивості завдання
Олександр Дерк,

Відповіді:


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

Але це здається мені дуже дивним, перш за все використання асинхронізації та очікування у виборі. Відповідно до цієї відповіді Стівена Клірі, я мав би змогу їх відмовити.

Дзвінок на адресу Selectдійсний. Ці два рядки по суті однакові:

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

(Існує незначна різниця щодо того, як буде викинуто синхронний виняток ProcessEventAsync, але в контексті цього коду це зовсім не має значення.)

Потім другий Select, який вибирає результат. Чи це не означає, що завдання зовсім не асинхронізується і виконується синхронно (стільки зусиль ні за що), чи завдання буде виконано асинхронно, і коли воно виконано, решта запиту буде виконана?

Це означає, що запит блокується. Тож насправді це не асинхронно.

Розбийте його:

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

спочатку почне асинхронну операцію для кожної події. Тоді цей рядок:

                   .Select(t => t.Result)

буде чекати, коли ці операції завершаться по черзі (спочатку він чекає операції першої події, потім наступної, потім наступної тощо).

Ця частина мене не хвилює, оскільки вона блокує, а також може містити будь-які винятки AggregateException.

і чи повністю він такий?

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

Так, ці два приклади рівнозначні. Вони обидва починають усі асинхронні операції ( events.Select(...)), потім асинхронно чекають, коли всі операції завершаться в будь-якому порядку ( await Task.WhenAll(...)), а потім продовжують роботу з рештою ( Where...).

Обидва ці приклади відрізняються від вихідного коду. Оригінальний код блокується, і винятки включатимуть AggregateException.


Будьте привітні для того, щоб очистити це! Отже, замість винятків, загорнутих у AggregateExceptionI, я отримав би кілька окремих винятків у другому коді?
Олександр Дерк

1
@AlexanderDerck: Ні, і в старому, і в новому коді буде піднято лише перший виняток. Але з Resultцим би загорнутий AggregateException.
Стівен Клірі

Я отримую тупик в моєму контролері ASP.NET MVC за допомогою цього коду. Я вирішив це за допомогою Task.Run (…). У мене немає хороших почуттів щодо цього. Однак він закінчився прямо під час запуску тесту async xUnit. Що відбувається?
SuperJMN

2
@SuperJMN: Замініть stuff.Select(x => x.Result);наawait Task.WhenAll(stuff)
Стівен Клірі

1
@DanielS: Вони по суті однакові. Є деякі відмінності, такі як стан машини, захоплення контексту, поведінка синхронних винятків. Більше інформації на blog.stephencleary.com/2016/12/eliding-async-await.html
Стівен Клірі

25

Існуючий код працює, але блокує нитку.

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

створює нове завдання для кожної події, але

.Select(t => t.Result)

блокує нитку, яка чекає завершення кожного нового завдання.

З іншого боку, ваш код дає такий же результат, але зберігає асинхронність.

Лише один коментар до вашого першого коду. Ця лінія

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

створить єдине завдання, тому змінна повинна бути названа в однині.

Нарешті ваш останній код зробить те саме, але є більш лаконічним

Для довідки: Task.Wait / Task.WhenAll


Отже, перший блок коду насправді виконується синхронно?
Олександр Дерк

1
Так, оскільки доступ до Result створює Wait, який блокує нитку. З іншого боку, коли ви створюєте нове завдання, якого ви можете чекати.
tede24

1
Повертаючись до цього питання і дивлячись на ваше зауваження щодо назви tasksзмінної, ви абсолютно праві. Жахливий вибір, вони навіть не завдання, оскільки їх чекають відразу. Я просто залишу питання як би то не було
Олександр Дерк

13

З сучасними методами, доступними в Linq, це виглядає досить некрасиво:

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

Сподіваємось, що наступні версії .NET запропонують більш елегантні інструменти для обробки колекцій завдань і завдань колекцій.


12

Я використовував цей код:

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

подобається це:

var result = await sourceEnumerable.SelectAsync(async s=>await someFunction(s,other params));

5
Це просто обгортає існуючий функціонал більш незрозумілим чином imo
Олександр Дерк

Альтернативою є var результат = очікувати Task.WhenAll (sourceEnumerable.Select (async s => очікуємо деякихFunction (s, інших парам)). Він також працює, але це не LINQy
Siderite Zackwehdex

Чи не повинен Func<TSource, Task<TResult>> methodмістити other paramsзгаданий другий біт коду?
matramos

2
Додаткові параметри є зовнішніми, залежно від функції, яку я хочу виконати, вони не мають значення в контексті методу розширення.
Siderite Zackwehdex

4
Це прекрасний метод розширення. Не впевнений, чому це було визнано "більш незрозумілим" - це семантично аналогічно синхронному Select(), так це елегантний випуск.
nullPainter

11

Я вважаю за краще це як метод розширення:

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

Таким чином, що це можна використовувати з ланцюжком методів:

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

1
Ви не повинні викликати метод, Waitколи він насправді не чекає. Це створення завдання, яке є завершеним, коли всі завдання виконані. Назвіть це WhenAll, як Taskметод емулювання. Для методу це також безглуздо async. Просто зателефонуйте WhenAllі будьте з ним.
Сервіс

Трохи марної обгортки, на мою думку, коли вона просто називає оригінальний метод
Олександр Дерк

@ Сервій справедливий момент, але мені особливо не подобається жоден із варіантів імен. WhenAll здається, це звучить як подія, яка це не зовсім.
Дарил

3
Перевага @AlexanderDerck полягає в тому, що ви можете використовувати його в ланцюжку методів.
Дарил

1
@Daryl, оскільки WhenAllповертає оцінений список (він не оцінюється ліниво), може бути зроблений аргумент, щоб використовувати Task<T[]>тип return для позначення цього. Коли його чекають, це все одно зможе використовувати Linq, але також повідомляє, що це не лінь.
JAD
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.