Parallel.ForEach может вызвать исключение «Out Of Memory», если вы работаете с перечислимым с большим объектом

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

Тем не менее, я заметил, что я получаю исключение OutOfMemory . Я знаю, что Parallel.ForEach будет запрашивать партию перечислений, чтобы уменьшить стоимость накладных расходов, если есть один для того, чтобы отделить запросы (так что у вашего источника будет, скорее всего, следующая запись, кэшированная в памяти, если вы сделаете кучу запросов одновременно от их расстояния). Проблема связана с тем, что одна из записей, которые я возвращаю, представляет собой массив байтов размером 1 МБ, который кэширует, чтобы все адресное пространство было использовано (программа должна работать в режиме x86, поскольку целевая платформа будет 32-разрядной машина)

Есть ли способ отключить кеширование или сделать меньше для TPL?


Вот пример программы, чтобы показать проблему. Это должно быть скомпилировано в режиме x86, чтобы показать проблему, если она длится или не происходит на вашем компьютере, увеличивая размер массива (я обнаружил, что 1 << 20 занимает около 30 секунд на моей машине и 4 << 20 был почти мгновенным)

 class Program { static void Main(string[] args) { Parallel.ForEach(CreateData(), (data) => { data[0] = 1; }); } static IEnumerable CreateData() { while (true) { yield return new byte[1 << 20]; //1Mb array } } } 

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

Но если задача не связана с ЦП, то Parallel.ForEach продолжает запускать задачи, пытаясь удержать все процессоры. Но независимо от того, сколько задач выполняется параллельно, всегда есть больше неиспользуемой мощности процессора, и поэтому он продолжает создавать задачи.

Как вы можете определить, связана ли ваша задача с CPU? Надеюсь, просто осмотрев его. Если вы факторизуете простые числа, это очевидно. Но другие случаи не столь очевидны. Эмпирический способ определить, связана ли ваша задача с CPU – ограничить максимальную степень параллелизма ParallelOptions.MaximumDegreeOfParallelism и наблюдать за тем, как работает ваша программа. Если ваша задача связана с процессором, вы должны увидеть такой шаблон в четырехъядерной системе:

  • ParallelOptions.MaximumDegreeOfParallelism = 1 : используйте один полный процессор или 25% загрузки процессора
  • ParallelOptions.MaximumDegreeOfParallelism = 2 : используйте два процессора или 50% использования ЦП
  • ParallelOptions.MaximumDegreeOfParallelism = 4 : используйте все процессоры или 100% использование ЦП

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

Но если я запустил образец приложения на своем Intel i7, я получаю около 20% загрузки процессора независимо от того, какую максимальную степень параллелизма я устанавливаю. Почему это? Так много памяти выделяется, что сборщик мусора блокирует streamи. Приложение привязано к ресурсам, а ресурс – это память.

Точно так же задача, связанная с I / O-привязкой, которая выполняет длительные запросы к серверу базы данных, также никогда не сможет эффективно использовать все ресурсы центрального процессора, доступные на локальном компьютере. И в таких случаях планировщик задач не может «знать, когда прекратить» запуск новых задач.

Если ваша задача не привязана к процессору или использование ЦП не масштабируется линейно с максимальной степенью параллелизма, тогда вам следует советовать Parallel.ForEach не запускать слишком много задач одновременно. Самый простой способ – указать число, которое допускает некоторый параллелизм для перекрытия задач, связанных с I / O-привязкой, но не настолько, чтобы вы подавляли потребность локального компьютера в ресурсах или перенаправляли любые удаленные серверы. Для получения наилучших результатов используются проб и ошибок:

 static void Main(string[] args) { Parallel.ForEach(CreateData(), new ParallelOptions { MaxDegreeOfParallelism = 4 }, (data) => { data[0] = 1; }); } 

Итак, хотя то, что предложил Рик, определенно важный момент, еще одна вещь, о которой я думаю, отсутствует, – это обсуждение раздела .

Parallel::ForEach будет использовать реализацию Partitioner по умолчанию, которая для IEnumerable которая не имеет известной длины, будет использовать страtagsю разбиения кусков. Это означает, что каждый рабочий stream, который Parallel::ForEach будет использовать для работы с набором данных, будет считывать некоторое количество элементов из IEnumerable которые затем будут обрабатываться только этим streamом (игнорируя сейчас кражу работы) , Он делает это, чтобы сэкономить расходы, связанные с постоянным возвратом к источнику, и выделить новую работу и запланировать ее для другого рабочего streamа. Поэтому, как правило, это хорошо. Однако в вашем конкретном сценарии представьте, что вы на четырехъядерном ядре, и вы установили MaxDegreeOfParallelism в 4 streamа для вашей работы, и теперь каждый из них вытаскивает кусок из 100 элементов из вашего IEnumerable . Ну, это 100-400 мегабайт прямо именно для этой конкретной рабочей нитки, верно?

Итак, как вы это решаете? Легко, вы пишете пользовательскую реализацию Partitioner . Теперь, chunking по-прежнему полезен в вашем случае, поэтому вы, вероятно, не хотите идти с одной страtagsей разбиения элементов, потому что тогда вы должны ввести накладные расходы со всей необходимой для этого необходимой координации задач. Вместо этого я бы написал настраиваемую версию, которую вы можете настроить с помощью приложения, пока не найдете оптимальный баланс для своей рабочей нагрузки. Хорошей новостью является то, что при написании такой реализации довольно прямолинейно, вам на самом деле не нужно даже писать ее сами, потому что команда PFX уже сделала это и поместила ее в проект параллельных программных образцов .

Эта проблема имеет все, что связано с разделителями, а не со степенью параллелизма. Решением является внедрение пользовательского разделителя данных.

Если dataset большой, кажется, что mono-реализация TPL гарантированно исчерпана. Это случилось со мной в последнее время (по сути, я работал над этим циклом и обнаружил, что память увеличилась линейно, пока она не дала мне исключение OOM ).

После отслеживания проблемы я обнаружил, что по умолчанию mono будет делить счетчик с помощью classа EnumerablePartitioner. Этот class имеет поведение в том, что каждый раз, когда он выдаёт данные заданию, он «куски» данных постоянно увеличивающимся (и неизменным) коэффициентом 2. Таким образом, когда в первый раз задача запрашивает данные, она получает кусок размера 1, в следующий раз размером 2 * 1 = 2, в следующий раз 2 * 2 = 4, затем 2 * 4 = 8 и т. Д. И т. Д. Результат состоит в том, что количество данных, переданных задаче, и, следовательно, хранится в память одновременно увеличивается с длиной задачи, и если много данных обрабатывается, неизбежно возникает исключение из памяти.

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

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

https://gist.github.com/evolvedmicrobe/7997971

Просто сначала создайте этот class и передайте его в Parallel.For вместо перечислимого

  • Сжатие PDF с помощью iTextSharp
  • Скрыть форму, а не закрывать при нажатии кнопки «Закрыть»
  • Слабая модель обработчика событий для использования с lambdas
  • Почему метод classа C #, реализованный в classе, должен быть общедоступным?
  • В чем разница между NULL, '\ 0' и 0
  • C # «Параметр недействителен». Создание нового растрового изображения
  • вопросы об управлении именами в C ++
  • Вызов метода classа с помощью указателя classа NULL
  • В чем смысл константы в конце функции-члена?
  • Reflection - получить имя атрибута и значение свойства
  • Преобразование String to Date в .NET, если мой формат даты в формате YYYYMMDD
  • Давайте будем гением компьютера.