it-swarm.com.ru

Должен ли я всегда использовать параллельный поток, когда это возможно?

С Java 8 и лямбдами легко перебирать коллекции как потоки, и так же просто использовать параллельный поток. Два примера из документы , второй - с использованием parallelStream:

myShapesCollection.stream()
    .filter(e -> e.getColor() == Color.RED)
    .forEach(e -> System.out.println(e.getName()));

myShapesCollection.parallelStream() // <-- This one uses parallel
    .filter(e -> e.getColor() == Color.RED)
    .forEach(e -> System.out.println(e.getName()));

Пока меня не волнует порядок, всегда ли будет выгодно использовать параллель? Казалось бы, быстрее разделить работу на большее количество ядер.

Есть ли другие соображения? Когда следует использовать параллельный поток и когда следует использовать непараллельный?

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

405
Matsemann

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

  • У меня есть огромное количество элементов для обработки (или обработка каждого элемента занимает много времени и распараллеливается)

  • У меня проблема с производительностью в первую очередь

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

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

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

В любом случае, мера, не угадай! Только измерение покажет вам, стоит ли параллелизм того или нет.

594
JB Nizet

Stream API был разработан, чтобы упростить написание вычислений таким способом, который был абстрагирован от способа их выполнения, что облегчало переключение между последовательным и параллельным.

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

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

Кроме того, обратите внимание, что параллелизм также часто выявляет недетерминизм в вычислениях, который часто скрыт последовательными реализациями; иногда это не имеет значения или может быть смягчено путем ограничения задействованных операций (то есть операторы редукции должны быть без сохранения состояния и ассоциативными).

В действительности иногда параллелизм ускоряет ваши вычисления, иногда нет, а иногда даже замедляет их. Лучше всего сначала разрабатывать с использованием последовательного выполнения, а затем применять параллелизм, когда (A) вы знаете, что на самом деле выгода от повышения производительности и (B) что это действительно повысит производительность. (А) это бизнес-проблема, а не техническая. Если вы эксперт по производительности, вы, как правило, сможете взглянуть на код и определить (B), но разумный путь - это измерить. (И даже не беспокойтесь, пока не убедитесь в (A); если код достаточно быстрый, лучше применить свои мозговые циклы в другом месте.)

Простейшей моделью производительности для параллелизма является модель "NQ", где N - количество элементов, а Q - вычисление на элемент. Как правило, продукт NQ должен превышать пороговое значение, прежде чем вы начнете получать выигрыш в производительности. Для задачи с низким Q, такой как "сложение чисел от 1 до N", вы обычно увидите безубыточность между N = 1000 и N = 10000. При проблемах с более высоким Q вы увидите безубыточности при более низких порогах.

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

204
Brian Goetz

Я смотрел одну из презентаций из Брайана Гетца (Java Language Architect и руководство по спецификации для Lambda Выражения) . Он подробно объясняет следующие 4 момента, которые следует учитывать перед переходом к распараллеливанию:

Расходы на разделение/разложение
- Иногда разделение обходится дороже, чем просто работа!
Расходы на диспетчеризацию/управление задачами
- Может выполнять много работы за время, необходимое для ручной работы в другом потоке.
Результат комбинированных затрат
- Иногда комбинация предполагает копирование большого количества данных. Например, добавление чисел дешево, тогда как объединение наборов стоит дорого.
[. .____] Местность
- Слон в комнате. Это важный момент, который каждый может упустить. Вы должны учитывать пропуски в кеше, если процессор ожидает данные из-за пропусков в кеше, вы ничего не получите от распараллеливания. Вот почему источники на основе массива распараллеливаются лучше всего, так как следующие индексы (рядом с текущим индексом) кэшируются, и вероятность того, что ЦП будет пропускать кэш, будет меньше.

Он также упоминает относительно простую формулу для определения вероятности параллельного ускорения.

Модель NQ :

N x Q > 10000

, где,
N = количество элементов данных
Q = объем работы на единицу

50
Ram Patra

JB ударил гвоздь по голове. Единственное, что я могу добавить, это то, что Java 8 не выполняет чисто параллельную обработку, а выполняет paraquential . Да, я написал статью и уже тридцать лет занимаюсь F/J, поэтому понимаю проблему.

11
edharned

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

Как правило, выигрыш в производительности от параллелизма является наилучшим для потоков через экземпляры ArrayList, HashMap, HashSet и ConcurrentHashMap; массивы; int диапазоны; и long диапазоны. Общим для этих структур данных является то, что все они могут быть точно и дешево разбиты на поддиапазоны любых желаемых размеров, что позволяет легко распределять работу между параллельными потоками. Абстракция, используемая библиотекой потоков для выполнения этой задачи, является сплитератором, который возвращается методом spliterator для Stream и Iterable.

Другим важным фактором, который объединяет все эти структуры данных, является то, что они обеспечивают отличное расположение ссылок при последовательной обработке: последовательные ссылки на элементы хранятся вместе в памяти. Объекты, на которые ссылаются эти ссылки, могут не находиться близко друг к другу в памяти, что уменьшает локальность ссылок. Ссылочное местоположение оказывается критически важным для распараллеливания массовых операций: без него потоки проводят большую часть своего времени в режиме ожидания, ожидая передачи данных из памяти в кэш процессора. Структуры данных с наилучшим местоположением ссылок являются примитивными массивами, потому что сами данные хранятся непрерывно в памяти.

Источник: № 48. Будьте осторожны при параллельном и эффективном создании потоков Java 3e Джошуа Блоха

1
ruhong