Task.Factory.StartNew или Parallel.ForEach для многих длительных задач?

Возможный дубликат:
Parallel.ForEach и Task.Factory .НачатьНовый

Мне нужно запускать около 1000 задач в ThreadPool каждую ночь (число может увеличиться в будущем). Каждая задача выполняет длительную операцию (чтение данных из веб-службы) и не загружает ЦП. Async I/O не подходит для этого конкретного варианта использования.

Учитывая IList<string> параметров, мне нужно DoSomething(string x). Я пытаюсь выбрать между двумя следующими вариантами:

IList<Task> tasks = new List<Task>();
foreach (var p in parameters)
{
    tasks.Add(Task.Factory.StartNew(() => DoSomething(p), TaskCreationOptions.LongRunning));
}
Task.WaitAll(tasks.ToArray());

OR

Parallel.ForEach(parameters, new ParallelOptions {MaxDegreeOfParallelism = Environment.ProcessorCount*32}, DoSomething);

Какой вариант лучше и почему?

Примечание.

Ответ должен включать сравнение использования TaskCreationOptions.LongRunning и MaxDegreeOfParallelism = Environment.ProcessorCount * SomeConstant.


person Zaid Masud    schedule 21.05.2012    source источник
comment
10 000 потоков кажутся мне довольно неработоспособными. Даже с TaskCreationOptions.LongRunning планировщик задач не будет выделять потоки для всех этих задач одновременно. Вы закончите тем, что работает только подмножество, а остальные ждут, пока они не будут завершены. Вы никак не можете реорганизовать свой код?   -  person GazTheDestroyer    schedule 21.05.2012
comment
Возможный дубликат: stackoverflow.com/q/5009181/21727   -  person mbeckish    schedule 21.05.2012
comment
@zooone9243 Zooone9243 Вы понимаете, что 10000 потоков займут 10 ГБ памяти только в стеке? Гораздо больше, если ваши потоки работают с объектами. Я думаю, что GazTheDestroyer прав, вам может понадобиться найти способ рефакторинга вашего кода. Использование async io для сокетов отбрасывает работу на ядро, и вы можете, по крайней мере, сэкономить память, имея гораздо меньше потоков.   -  person Christopher Currens    schedule 21.05.2012
comment
Выполнение 1000 потоков потребует почти 1 гигабайта виртуальной памяти при размере стека по умолчанию 1 мегабайт. Очевидно, что при использовании TPL или PLINQ вы не будете запускать столько потоков, но я просто хотел указать на тот факт, что поток, даже если он находится в спящем режиме, имеет стоимость для процесса.   -  person Martin Liversage    schedule 21.05.2012
comment
@ChristopherCurrens и всем остальным не нужно зацикливаться или увлекаться этим вопросом. Это отвлекает внимание от реального вопроса. Я никоим образом не подразумеваю, что 10 000 потоков должны выполняться одновременно. Конечно, они могут быть разбиты на разделы и, конечно же, они должны эффективно использовать ThreadPool, вся цель которого не в том, чтобы занимать 10 ГБ в пространстве стека.   -  person Zaid Masud    schedule 21.05.2012
comment
@mbeckish не является дубликатом - другой вопрос не ожидает всех задач и не указывает LongRunning / MaxDegreeOfParallelism. Это принципиально другая проблема.   -  person Zaid Masud    schedule 21.05.2012
comment
@MartinLiversage да, если все они работают одновременно, чего мы явно не хотим. Я ищу реализацию, которая даст мне наилучшую производительность с учетом проблемы без изменения проблемы.   -  person Zaid Masud    schedule 21.05.2012
comment
@zooone9243: Возможно, я немного придираюсь, и это просто вопрос языка, но на 4-ядерной машине одновременно работают только 4 потока. Вы по-прежнему можете создать 1000 потоков (если только вы не исчерпали пространство памяти), но правильное решение — использовать потоки из пула именно так, как вы собираетесь это делать. Меня смущает тот факт, что мне нужно запустить около 1000 потоков.   -  person Martin Liversage    schedule 21.05.2012
comment
@MartinLiversage Я понимаю твою точку зрения. Я обновил текст, надеюсь, что он понятнее? Спасибо, что указали на это.   -  person Zaid Masud    schedule 21.05.2012
comment
Разве вы не можете использовать асинхронный ввод-вывод и сохранять состояние в некоторых объектах вместо того, чтобы иметь тысячи потоков, которые большую часть времени ничего не делают, кроме как потребляют ресурсы?   -  person Wormbo    schedule 21.05.2012
comment
@Wormbo спасибо за предложение, но не вдаваясь в подробности, извините, асинхронный ввод-вывод не подходит для этого конкретного случая.   -  person Zaid Masud    schedule 21.05.2012
comment
Ребята, есть некоторая документация, которую я нахожу, которая подразумевает, что первый вариант может фактически создать 1000 потоков, но я не смог полностью проверить это... см. здесь stackoverflow.com/questions/3105988/. Если это правда, это исключает первый вариант для моего варианта использования.   -  person Zaid Masud    schedule 21.05.2012
comment
Пожалуйста, не говорите нам, что асинхронный ввод-вывод невозможен, не объяснив это. Это похоже на проблему X/Y, если я когда-либо слышал о ней. Асинхронный ввод-вывод — это правильный способ выполнения задач такого типа. Если вы уверены, что это неприменимо в вашем случае, объясните свою проблему, чтобы мы действительно могли попытаться предложить наилучшее возможное решение.   -  person Aaronaught    schedule 21.05.2012
comment
@Aaronaught Короче говоря, оболочка веб-службы, которую мы должны использовать в этом бизнес-контексте, не поддерживает асинхронный ввод-вывод.   -  person Zaid Masud    schedule 21.05.2012
comment
Затем измените его так, чтобы он работал. Это проблема архитектуры, а не проблема производительности. Вы не получите приемлемой производительности, используя TPL (который включает в себя как Task, так и Parallel). В лучшем случае вы просите выбрать между меньшим из двух серьезных зол.   -  person Aaronaught    schedule 21.05.2012
comment
Кстати, я понимаю, что это старый вопрос: Task/Parallel - это функции 4.0. async — это функция версии 4.5 (да, я понимаю, что была CTP). Таким образом, Бог может просто повелеть, чтобы в коде были только настоящие функции 4.0. Или, как вопрошающий упоминает, что библиотека веб-сервисов, которую необходимо использовать, может быть сторонней, и нет возможности изменить ее и украсить все с помощью async/awaits везде.   -  person Mike    schedule 14.03.2013
comment
Здесь следует отметить следующее: если вы используете Parallel.ForEach при длительном выполнении (задачи, связанные с вводом-выводом), планировщик потоков становится нетерпеливым. Предполагается, что причина медленного выполнения заключается в том, что задачи слишком интенсивно используют ЦП, поэтому он начинает добавлять потоки в пул потоков со скоростью 2 в минуту. Это в основном пропускает потоки таким образом, пока параллельный foreach не будет завершен.   -  person Steven Padfield    schedule 19.03.2013


Ответы (3)


Возможно, вы этого не знаете, но члены класса Parallel — это просто (сложные) оболочки вокруг объектов Task. Если вам интересно, класс Parallel создает объекты Task с помощью TaskCreationOptions.None. Однако MaxDegreeOfParallelism повлияет на эти объекты задачи, независимо от того, какие параметры создания будут переданы конструктору объекта задачи.

TaskCreationOptions.LongRunning дает «подсказку» базовому TaskScheduler, что он может работать лучше с превышением количества потоков. Переподписка хороша для потоков с высокой задержкой, например, ввода-вывода, потому что она назначит более одного потока (да, потока, а не задачи) одному ядру, так что ему всегда будет чем заняться, вместо того, чтобы ждать операция для завершения, пока поток находится в состоянии ожидания. На TaskScheduler, который использует ThreadPool, он будет запускать задачи LongRunning в своем собственном выделенном потоке (единственный случай, когда у вас есть поток для каждой задачи), в противном случае он будет работать нормально, с планированием и перехватом работы ( собственно, чего вы тут хотите)

MaxDegreeOfParallelism управляет количеством одновременно выполняемых операций. Это похоже на указание максимального количества разделов, на которые будут разделены и обработаны данные. Если бы можно было указать TaskCreationOptions.LongRunning, все, что нужно было бы сделать, это ограничить количество одновременно выполняемых задач, подобно TaskScheduler, максимальный уровень параллелизма которого установлен на это значение, аналогично этому примеру.

Вам может понадобиться Parallel.ForEach. Однако добавление MaxDegreeOfParallelism, равного такому большому числу, на самом деле не гарантирует, что одновременно будет выполняться такое количество потоков, поскольку задачи по-прежнему будут контролироваться ThreadPoolTaskScheduler. Этот планировщик будет уменьшать количество потоков, работающих одновременно, до минимально возможного количества, что, я полагаю, является самой большой разницей между двумя методами. Вы можете написать (и указать) свой собственный TaskScheduler, который будет имитировать максимальную степень поведения параллелизма и иметь лучшее из обоих миров, но я сомневаюсь, что вы заинтересованы в этом.

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

Реальным решением было бы найти способ реализовать асинхронный ввод-вывод; так как я не знаю ситуацию, я не думаю, что могу быть более полезным, чем это. Ваша программа (чтение, поток) продолжит выполнение, а ядро ​​будет ждать завершения операции ввода-вывода (это также известно как использование портов завершения ввода-вывода). Поскольку поток не находится в состоянии ожидания, среда выполнения может выполнять больше работы с меньшим количеством потоков, что обычно приводит к оптимальному соотношению между количеством ядер и количеством потоков. Добавление большего количества потоков, как бы мне этого ни хотелось, не означает повышения производительности (на самом деле, это часто может повредить производительности из-за таких вещей, как переключение контекста).

Однако весь этот ответ бесполезен для определения окончательного ответа на ваш вопрос, хотя я надеюсь, что он даст вам необходимое направление. Вы не узнаете, что работает лучше, пока не профилируете это. Если вы не попробуете их оба (я должен уточнить, что я имею в виду задачу без опции LongRunning, позволяющую планировщику обрабатывать переключение потоков) и не профилировать их, чтобы определить, что лучше всего подходит для вашего конкретного варианта использования, ты недооцениваешь себя.

person Christopher Currens    schedule 21.05.2012
comment
Спасибо за отличный ответ. Интересно, почему, если класс Parallel создает объекты Task, как получается, что он может создавать потоки переднего плана по сравнению с библиотекой задач, которая создает фоновые потоки и, похоже, не дает вам возможности создавать потоки переднего плана? - person Zaid Masud; 22.05.2012
comment
@ Zooone9243 - На самом деле это не создание потоков переднего плана. Вместо этого он просто вызывает функцию Wait(), которая блокирует выполнение до тех пор, пока оно не будет завершено или отменено. - person Christopher Currens; 22.05.2012
comment
@zooone9243 - Это немного сложнее, чем я представляю. Если вы хотите получить хорошее представление о внутренней работе, я рекомендую вам ознакомиться с справочным источником .NET - person Christopher Currens; 22.05.2012

Оба варианта совершенно не подходят для вашего сценария.

TaskCreationOptions.LongRunning, безусловно, лучший выбор для задач, не связанных с ЦП, поскольку TPL (Parallel классы/расширения) почти исключительно предназначены для максимизации пропускной способности операции, связанной с ЦП, за счет ее выполнения на нескольких ядрах (а не потоках).

Однако 1000 задач — неприемлемое число для этого. Независимо от того, работают ли они все одновременно, это не совсем проблема; даже 100 потоков, ожидающих синхронного ввода-вывода, — неприемлемая ситуация. Как предполагает один из комментариев, ваше приложение будет использовать огромное количество памяти и в конечном итоге будет тратить почти все свое время на переключение контекста. TPL не предназначен для этого масштаба.

Если ваши операции связаны с вводом-выводом, а если вы используете веб-сервисы, то так и есть, тогда асинхронный ввод-вывод — это не только правильное решение, но и единственное решение. . Если вам нужно изменить архитектуру вашего кода (например, добавить асинхронные методы к основным интерфейсам, которых изначально не было), сделайте это, потому что порты завершения ввода-вывода — это < em>единственный механизм в Windows или .NET, который может должным образом поддерживать этот конкретный тип параллелизма.

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

person Aaronaught    schedule 21.05.2012
comment
Я никогда не слышал о ситуации, когда асинхронный ввод-вывод был каким-то образом «не вариант»… каждый вызов веб-службы требует дорогостоящего рукопожатия. Установление соединения — настоящий убийца, едва ли не больший, чем сами звонки. Мои знания о портах завершения ввода-вывода ограничены, можно ли их использовать в этом сценарии? Если у вас есть хорошие ссылки на них, пожалуйста, поделитесь. Спасибо. - person Zaid Masud; 21.05.2012
comment
@ Zooone9243, я не понимаю, почему это должно означать, что вы не можете использовать асинхронный ввод-вывод. И трудно сказать вам, как именно это сделать, если вы не расскажете нам больше. - person svick; 21.05.2012
comment
@svick Мне нужно узнать больше об этих портах завершения ввода-вывода ... мы говорим об использовании неуправляемых потоков ввода-вывода Windows, как обсуждается здесь? blogs.msdn.com/b/ericeil/archive/2008/06/20/ - person Zaid Masud; 21.05.2012
comment
@ Zooone9243, нет, это означает использование методов BeginXxx()/EndXxx() вместо только метода Xxx(). О каком именно методе (ах) мы говорим, зависит от того, что именно вы делаете (это могут быть методы на WebRequest или Socket или, может быть, что-то еще). Затем методы Begin/End используют внутренние порты завершения ввода-вывода. - person svick; 21.05.2012
comment
@zooone9243: Возможно, я неправильно истолковал здесь значение веб-службы, но, по моему опыту, вся предпосылка веб-службы заключается в том, что она использует стандартный веб-протокол и формат (например, SOAP, XML, JSON, поверх HTTP или HTTPS). ), для доступа к которым вам не нужна собственная библиотека. Это какая-то совершенно непрозрачная служба RPC с двоичным кодированием, для которой у вас нет исходного кода или спецификаций? - person Aaronaught; 21.05.2012

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

    var servicePointManager = System.Net.ServicePointManager.FindServicePoint(Uri);
    servicePointManager.ConnectionLimit = 250;

Обратите внимание, что вы должны вызывать это один раз для каждого уникального URL-адреса, к которому вы подключаетесь.

person Aaron Anodide    schedule 21.05.2012