Понимание Task.WhenAll, код ведет себя не очевидным образом

Проблема следующая представьте что у нас вызовы GetTask и GetOtherTask являются долгими по времени, что бы ускорить время ожидания мы их объединяем и вызываем через Task.WhenAll, но проблема в том что вызов GetTask вызывается не 2 раза (по количеству элементов в коллекции что видно в коде) а целых 5, и как результат у нас происходит лишних 3 вызова.

namespace Example {
    class Program {
        static async Task Main(string[] args) {
            var firstTask = GetOtherTask();

            var secondTasks = Enumerable.Range(0,2).Select( async x=> {
                return await GetTask(); /// <--- Сюда заходит 5 раз хотя должен только 2
            });

            var tasks = secondTasks.Union(new[] { firstTask });
            await Task.WhenAll(tasks);

            var result = tasks.Select(x => {
                return x;
            }).ToArray();

        }

        static async Task<int> GetTask() {
            await Task.Delay(100);
            return 0;
        }

        static async Task<int> GetOtherTask() {
            await Task.Delay(100);
            return 1;
        }
    }
}

Ответы (1 шт):

Автор решения: Grundy

Проблема заключается в ленивости.

tasks - это на самом деле итератор.

  1. Первый раз он материализуется при передаче в Task.WhenAll
  2. Второй раз - при вызове .ToArray после .Select

Чтобы этого избежать, достаточно материализовать его один раз перед передачей в Task.WhenAll и передавать уже результат.


Также можно сразу материализовать var secondTasks = (...).ToArray()


Альтернативным решением может стать использование значения возвращаемого методом Task.WhenAll - массив значений переданных тасков:

var tasks = secondTasks.Union(new[] { firstTask });
var taskResults = await Task.WhenAll(tasks);

var result = taskResults.Select(x => {
    return x;
}).ToArray();

В этом случае возможно не нужна последняя строчка с result.

→ Ссылка