Читаю я как-то главу 6.8 “Handling Parallel Tasks as They Complete” книги Joe Mayo “C# Cookbook” и встречаю такой абзац:
You thought calling Task.WhenAny
would be an efficient use resources for processing results as they complete, but cost and performance are terrible.
Минуточку, уважаемый Джо! А тут я попрошу вас поподробнее. У меня на проде используется этот Task.WhenAny
… Но в подробности Джо не пускается, рекомендуя вместо использовать Task.WhenAny
Task.WhenAll
, утверждая, что у Task.WhenAll
сложность О(1), т.к. вместо N последовательных операций выполняется как-бы одна (все N операций выполняются параллельно).
Далее Джо утверждает, что в паттерне:
1 | while(tasks.Any()) |
всё происходит не так как ты ожидаешь:
The reality is that each subsequent loop starts a brand-new set of tasks. This is how async works - you can await
a task multiple times, but each await
starts a new task. That means the code continiously starts new instances of remaining tasks on every loop. This looping pattern, with Task.WhenAny
, doesn’t result in the O(1) performance you might have expected, like with Task.WhenAll
, but rather O(N^2).
А это уже плохой сигнал, как говорит Егор Иванов :( Далее Джо пугает, что это скажется не только на плохой производительности приложения, но и может породить избыточную сетевую активность и нагрузить конечный сервер бесполезной работой. А представляете как эти избыточные сетевые запросы отразятся и сколько будут стоить в облачном сервисе?!? Короче Джо признаёт Task.WhenAny
антипаттерном в том виде, что он приведён выше, если он только не используется для малого числа задач. Джо советует использовать Task.WhenAny
только для случая, когда нас интересует только задача победитель, а на остальные можно забить и не дожидаться/продолжать их обработку. Тогда Task.WhenAny
тоже будет иметь производительность со сложностью O(1).
Ну и завершает главу Джо таким абзацем:
Task.WhenAny runs those tasks in parallel and the first task to complete comes back. The code processes that task and returns. The side effect in this solution is that tasks other the first that returned continue running. However, you don’t have access to them because only one task is returned. For this scenario, you don’t care about those tasks but should stop them to avoid using more resources than necessary. You can learn how to do that in the next section on cancelling tasks.
Вот такие пироги! Побочный эффект, понимаешь! Сейчас проверим, что за побочный эффект. Для этого наваяем такой класс:
1 | public class ExperimentalTask |
и задействуем его таким кодом:
1 | Console.WriteLine("Experiment is starting"); |
Запустим приложение и увидим такие результаты:
1 | Experiment is starting |
Так, а где побочный эффект? Где “the code continiously starts new instances of remaining tasks on every loop”? Про какую реальность утверждает Джо “The reality is that each subsequent loop starts a brand-new set of tasks”? При исследовании Task.WhenAny паттерна на сайте Майкрософт Обработка асинхронных задач по мере завершения (C#) находим примечание:
Внимание!
Можно использоватьWhenAny
в цикле, как описано в примере, для решения проблем, которые включают небольшое число задач. Однако когда требуется обработка большого числа задач, другие методы будут более эффективны. Дополнительные сведения и примеры см. в разделе Обработка задач по мере их завершения.
Предлагаю углубиться в эту тему через мой перевод этой статьи.