Task.Factory.StartNew или Parallel.ForEach для многих долгосрочных задач?
Возможный дубликат:
Parallel.ForEach vs Task.Factory.StartNew
Мне нужно выполнить около 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());
ИЛИ ЖЕ
Parallel.ForEach(parameters, new ParallelOptions {MaxDegreeOfParallelism = Environment.ProcessorCount*32}, DoSomething);
Какой вариант лучше и почему?
Замечания:
Ответ должен включать сравнение между использованием TaskCreationOptions.LongRunning
а также MaxDegreeOfParallelism = Environment.ProcessorCount * SomeConstant
,
3 ответа
Возможно, вы не знаете об этом, но члены в Parallel
класс просто (сложные) обертки вокруг Task
объекты. Если вам интересно, то Parallel
класс создает Task
объекты с TaskCreationOptions.None
, Тем не менее MaxDegreeOfParallelism
будет влиять на эти объекты задачи независимо от того, какие параметры создания были переданы в конструктор объекта задачи.
TaskCreationOptions.LongRunning
дает "подсказку" к основному TaskScheduler
что он может работать лучше с переподпиской потоков. Избыточная подписка хороша для потоков с высокой задержкой, например, для ввода-вывода, поскольку она назначит более одного потока (да, потока, а не задачи) одному ядру, так что ему всегда будет чем заняться, вместо того, чтобы ждать операция, которая завершается, когда поток находится в состоянии ожидания. На TaskScheduler
который использует ThreadPool
, он будет запускать задачи LongRunning в своем собственном выделенном потоке (единственный случай, когда у вас есть поток на задачу), в противном случае он будет работать нормально, с планированием и кражей работы (действительно, что вы здесь хотите в любом случае)
MaxDegreeOfParallelism
контролирует количество одновременных операций. Это похоже на указание максимального количества разделов, на которые будут разбиваться и обрабатываться данные. Если TaskCreationOptions.LongRunning
можно было указать, все, что нужно будет сделать, это ограничить количество задач, выполняемых за один раз, аналогично TaskScheduler
чей максимальный уровень параллелизма установлен на это значение, как в этом примере.
Вы могли бы хотеть Parallel.ForEach
, Тем не менее, добавив MaxDegreeOfParallelism
равное такому большому числу на самом деле не гарантирует, что одновременно будет запущено столько потоков, так как задачи по-прежнему будут контролироваться ThreadPoolTaskScheduler
, В этом планировщике количество одновременно запущенных потоков будет наименьшим из возможных значений, что, я полагаю, является самой большой разницей между этими двумя методами. Вы можете написать (и указать) свой TaskScheduler
это имитировало бы максимальную степень параллелизма и имело бы лучшее из обоих миров, но я сомневаюсь, что вы заинтересованы в этом.
Я предполагаю, что, в зависимости от задержки и количества фактических запросов, которые вам нужно сделать, использование задач будет работать лучше во многих (?) Случаях, хотя в конечном итоге будет использоваться больше памяти, а параллель будет более последовательной в использовании ресурсов. Конечно, асинхронный ввод-вывод будет работать чудовищно лучше, чем любой из этих двух вариантов, но я понимаю, что вы не можете сделать это, потому что вы используете устаревшие библиотеки. Так что, к сожалению, вы застрянете с посредственным исполнением независимо от того, кого из них вы выбрали.
Реальным решением было бы выяснить способ осуществления асинхронного ввода-вывода; так как я не знаю ситуацию, я не думаю, что могу быть более полезным, чем это. Ваша программа (чтение, поток) продолжит выполнение, и ядро будет ожидать завершения операции ввода / вывода (это также называется использованием портов завершения ввода / вывода). Поскольку поток не находится в состоянии ожидания, среда выполнения может выполнять больше работы с меньшим количеством потоков, что обычно приводит к оптимальному соотношению между числом ядер и числом потоков. Добавление большего количества потоков, сколько бы я ни хотел, не означает лучшую производительность (на самом деле, это может часто ухудшать производительность из-за таких вещей, как переключение контекста).
Однако весь этот ответ бесполезен в определении окончательного ответа на ваш вопрос, хотя я надеюсь, что он даст вам необходимое направление. Вы не будете знать, что работает лучше, пока не профилируете это. Если вы не попробуете их обоих (я должен уточнить, что я имею в виду Задачу без опции LongRunning, позволяющей планировщику обрабатывать переключение потоков) и профилировать их, чтобы определить, что лучше для вашего конкретного варианта использования, вы продаете себя коротко.
Хотя это не прямое сравнение, я думаю, что это может вам помочь. Я делаю что-то похожее на то, что вы описываете (в моем случае я знаю, что на другом конце есть кластер серверов с балансировкой нагрузки, обслуживающий вызовы REST). Я получаю хорошие результаты, используя Parrallel.ForEach
ускорить оптимальное количество рабочих потоков при условии, что я также использую следующий код, чтобы сообщить моей операционной системе, что она может подключаться к большему количеству конечных точек.
var servicePointManager = System.Net.ServicePointManager.FindServicePoint(Uri);
servicePointManager.ConnectionLimit = 250;
Обратите внимание, что вы должны вызывать это один раз для каждого уникального URL, к которому вы подключаетесь.
Оба варианта совершенно не подходят для вашего сценария.
TaskCreationOptions.LongRunning
это, безусловно, лучший выбор для задач, которые не связаны с процессором, как TPL (Parallel
классы / расширения) почти исключительно предназначены для максимизации пропускной способности связанной с процессором операции, выполняя ее на нескольких ядрах (не потоках).
Тем не менее, 1000 задач является неприемлемым числом для этого. Являются ли они все запущенными одновременно, не совсем проблема; даже 100 потоков, ожидающих синхронного ввода-вывода, - ненадежная ситуация. Как говорится в одном из комментариев, ваше приложение будет использовать огромный объем памяти и в конечном итоге будет тратить почти все свое время на переключение контекста. TPL не предназначен для этого масштаба.
Если ваши операции связаны с вводом / выводом - и если вы используете веб-сервисы, то они есть - тогда асинхронный ввод / вывод - это не только правильное решение, это единственное решение. Если вам нужно перестроить часть кода (например, добавить асинхронные методы в основные интерфейсы, где их не было изначально), сделайте это, потому что порты завершения ввода / вывода являются единственным механизмом в Windows или.NET, который может должным образом поддерживать этот конкретный тип параллелизма.
Я никогда не слышал о ситуации, когда асинхронный ввод-вывод был как-то "не вариант". Я не могу даже представить какой-либо действительный вариант использования для этого ограничения. Если вы не можете использовать асинхронный ввод-вывод, это может указывать на серьезную проблему проектирования, которая должна быть исправлена как можно скорее.