Task.StartNew() против Parallel.ForEach: сценарий нескольких веб-запросов

Я прочитал все связанные вопросы в SO, но немного запутался в лучшем подходе для моего сценария, когда запускается несколько вызовов веб-службы.

У меня есть служба-агрегатор, которая принимает входные данные, анализирует и переводит их в несколько веб-запросов, выполняет вызовы веб-запросов (несвязанные, поэтому их можно запускать параллельно) и объединяет ответ, который отправляется обратно вызывающей стороне. Следующий код используется прямо сейчас -

list.ForEach((object obj) =>
{
     tasks.Add(Task.Factory.StartNew((object state) => 
     {
           this.ProcessRequest(obj);
     }, obj, CancellationToken.None,
     TaskCreationOptions.AttachedToParent, TaskScheduler.Default));
});
await Task.WhenAll(tasks);

await Task.WhenAll(tasks) взят из сообщения Скотта Хансельмана, где говорится, что

По словам Стивена, лучшим решением с точки зрения масштабируемости является использование преимуществ асинхронного ввода-вывода. Когда вы звоните по сети, нет причин (кроме удобства) блокировать потоки в ожидании ответа.

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

Другой альтернативой является использование Parallel.ForEach, который использует разделитель, но также блокирует вызов, что подходит для моего сценария.

Учитывая, что это все работа с асинхронным вводом-выводом, а не работа, связанная с процессором, а веб-запросы выполняются недолго (возврат максимум через 3 секунды), я склонен полагать, что существующий код достаточно хорош. Но обеспечит ли это лучшую пропускную способность, чем Parallel.ForEach? Parallel.ForEach, вероятно, использует минимальное количество задач из-за разделения и, следовательно, оптимального использования потоков (?). Я протестировал Parallel.ForEach с некоторыми локальными тестами, и он не стал лучше.

Цель состоит в том, чтобы сократить время ЦП и увеличить пропускную способность и, следовательно, лучшую масштабируемость. Есть ли лучший подход для параллельной обработки веб-запросов?

Ценим любые материалы, спасибо.

EDIT: метод ProcessRequest, показанный в примере кода, действительно использует HttpClient и его асинхронные методы для отправки запросов (PostAsync, GetAsync, PutAsync).


comment
Если ProcessRequest использует асинхронные методы, почему вы вызываете их внутри Task.Factory.StartNew? Вы можете просто добавить задачу, которую он возвращает, в свой список. Если вы на самом деле блокируете его внутри, не имеет значения, что вы используете асинхронные методы в его частях. Последний блокирующий вызов сводит на нет все преимущества   -  person Panagiotis Kanavos    schedule 05.06.2015
comment
кроме удобства ну, это довольно веская причина.   -  person usr    schedule 05.06.2015


Ответы (3)


делает вызовы веб-запросов (не связанные, поэтому могут запускаться параллельно)

На самом деле вам нужно называть их одновременно, а не параллельно. То есть «одновременно», а не «используя несколько потоков».

Существующий код потребляет слишком много потоков

Да, я тоже так думаю. :)

Учитывая, что это все работа «асинхронного ввода-вывода», а не работа «с привязкой к процессору».

Затем все это должно выполняться асинхронно и не с использованием параллелизма задач или другого параллельного кода.

Как указал Антии, вы должны сделать свой асинхронный код асинхронным:

public async Task ProcessRequestAsync(...);

Затем вы хотите использовать его с использованием асинхронного параллелизма (Task.WhenAll), а не параллельного параллелизма (StartNew/Run/Parallel):

await Task.WhenAll(list.Select(x => ProcessRequestAsync(x)));
person Stephen Cleary    schedule 05.06.2015
comment
Параллельно и параллельно - это синонимы. При использовании parallel в этом ответе кажется, что вы имели в виду многопоточность. Then it should all be done asynchronously, and not using TPL or parallel code. Не следует использовать StartNew или Run TPL; использование TPL для управления задачами, представляющими асинхронную работу, было бы хорошо, поскольку это фактически то, что вы показали. Вы не используете TPL, вы просто используете его по-другому. - person Servy; 05.06.2015
comment
Не согласен с параллельной и параллельной терминологией. Но вы правы насчет TPL; Я имел в виду параллелизм задач. - person Stephen Cleary; 05.06.2015
comment
Делать что-то параллельно — это делать несколько дел одновременно. Вы можете делать несколько вещей одновременно, используя несколько потоков или выполняя несколько изначально асинхронных операций одновременно. Обе операции приводят к параллелизму. Parallel класс в .NET имеет операции, все из которых включают многопоточность, а не какие-либо другие средства достижения параллелизма, но общая концепция параллелизма или параллельного выполнения никоим образом не специфична для нескольких потоков. Что заставило бы вас думать, что это будет? - person Servy; 05.06.2015
comment
Я согласен, что для общеупотребительного английского это синонимы. Но разработчикам полезно различать параллелизм, параллелизм и асинхронность. Я всегда использую параллелизм в качестве родительской концепции, а параллелизм и асинхронность описывают конкретные подходы. В противном случае терминология становится запутанной IMO. - person Stephen Cleary; 05.06.2015
comment
Но в термине «Параллелизм» — ни в его более широком английском контексте, ни даже в контексте программирования — нет ничего, что делало бы его специфичным для использования нескольких потоков. Параллелизм может быть достигнут за счет использования асинхронности или за счет использования многопоточности. Я согласен с тем, что здесь много похожих, но немного разных терминов, и их трудно уловить. Я просто говорю, что определения — это больше, чем параллелизм/параллелизм как синонимы (даже в контексте программирования), и что параллелизм может быть достигнут либо с многопоточностью, либо с асинхронностью. - person Servy; 05.06.2015
comment
Я бы сказал, что попытка объяснить людям, что асинхронность не может использоваться для достижения параллелизма, вызовет довольно много путаницы. Вы можете абсолютно использовать асинхронность для параллельной работы; вам не требуется использование нескольких потоков для параллельной работы. Если вы используете термин «многопоточность» вместо «параллельный», то у вас есть термин, конкретно описывающий параллелизм, достигаемый за счет использования нескольких потоков. - person Servy; 05.06.2015
comment
Как ни странно, я всегда использовал параллелизм в качестве родительской концепции с параллелизмом подконцепций для одновременного выполнения многопоточных действий и асинхронностью для параллельной ожидающей операции, не блокирующей потоки :) - person noseratio; 06.06.2015

Если вы привязаны к ЦП (вы - «Процессорное время достигает 100%»), вам необходимо уменьшить загрузку ЦП. Асинхронный ввод-вывод ничего не делает, чтобы помочь с этим. Во всяком случае, это вызывает немного большую загрузку ЦП (здесь незаметно).

Профилируйте приложение, чтобы увидеть, что занимает так много процессорного времени, и оптимизируйте этот код.

То, как вы инициируете параллелизм (параллельный, задачный, асинхронный ввод-вывод), никак не влияет на эффективность самого параллельного действия. Сеть не станет быстрее, если вы вызовете ее асинхронно. Это все еще то же самое оборудование. Также не меньшее использование процессора.

Экспериментально определите оптимальную степень параллелизма и выберите метод параллелизма, подходящий для этой степени. Если их несколько десятков, то с нитями все в порядке. Если это сотни, серьезно рассмотрите асинхронный ввод-вывод.

person usr    schedule 05.06.2015

Обтекание синхронных вызовов внутри Task.Factory.StartNew не дает вам никаких преимуществ асинхронности. Вы должны использовать правильные асинхронные функции для лучшей масштабируемости. Обратите внимание, как Скотт Хансельман делает асинхронные функции в посте, на который вы ссылаетесь.

Например

public async Task<bool> ValidateUrlAsync(string url)
{
    using(var response = (HttpWebResponse)await WebRequest.Create(url).GetResponseAsync())
    return response.StatusCode == HttpStatusCode.Ok;
}

Оформить заказ http://blogs.msdn.com/b/pfxteam/archive/2012/03/24/10287244.aspx

Итак, ваш метод ProcessRequest должен быть реализован как асинхронный, например

public async Task<bool> ProcessRequestAsync(...)

тогда вы можете просто

tasks.Add(this.ProcessRequestAsync(obj))

Если вы запускаете задачу с помощью Task.Factory.StartNew, она не работает как асинхронная, даже если ваш метод ProcessRequest выполняет внутренние асинхронные вызовы. Если вы хотите использовать Task.Factory, вы должны сделать свою лямбду также асинхронной, например:

tasks.Add(Task.Factory.StartNew(async (object state) => 
{
    await this.ProcessRequestAsync(obj);
}, obj, CancellationToken.None, TaskCreationOptions.AttachedToParent,   TaskScheduler.Default));
person Antti Leppänen    schedule 05.06.2015
comment
Вероятно, я пропустил упоминание ... на самом деле, функция ProcessRequest выполняет вызовы асинхронных версий API HttpClient - PostAsync, SendAsync и GetAsync на основе переданного запроса (obj). Обновлю вопрос. - person Lalman; 05.06.2015
comment
Добавляйте в список задач только асинхронные функции. Не используйте Task.Factory.StartNew. - person Antti Leppänen; 05.06.2015
comment
Он привязан к процессору. Асинхронный ввод-вывод не обеспечит большей пропускной способности. - person usr; 05.06.2015
comment
Что ж, он сказал: «Учитывая, что это все работа с асинхронным вводом-выводом, а не работа, связанная с процессором», и заявил, что он использует HttpClient для асинхронных веб-запросов. Как связан этот процессор? - person Antti Leppänen; 05.06.2015
comment
Это процессор связан просто тем фактом, что он доводит процессор до 100%. Это ограничивает пропускную способность, которую он получает. - person usr; 05.06.2015
comment
Ну, это потому, что он блокирует работу асинхронности с помощью Task.Factory.StartNew. - person Antti Leppänen; 05.06.2015
comment
Блокировка не потребляет ЦП (за исключением небольшой постоянной суммы). Блокировка отменяет планирование потока от ЦП. Не делайте ошибку, думая, что Thread.Sleep(100) сжигает 100 мс процессорного времени! Он сжигает около 0,1 мс накладных расходов на вызов ядра и переключение контекста. - person usr; 05.06.2015