Вопрос-ответ

Should I always use a parallel stream when possible?

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

С 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()));

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

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

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

Переведено автоматически
Ответ 1

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


  • Мне нужно обработать огромное количество элементов (или обработка каждого элемента требует времени и может быть распараллелена)


  • Во-первых, у меня проблема с производительностью


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


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

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

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

Ответ 2

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

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

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

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

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

(A) вы знаете, что на самом деле повышение производительности и

(B) что это действительно обеспечит повышенную производительность.

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

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

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

Ответ 3

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

Затраты на разделение / декомпозицию
– Иногда разделение обходится дороже, чем просто выполнение работы!

Затраты на отправку задач / управление
– Может выполнять большую работу за время, необходимое для передачи работы другому потоку.

Затраты на комбинацию результатов
– Иногда комбинирование предполагает копирование большого количества данных. Например, добавление чисел обходится дешево, тогда как объединение наборов обходится дорого.

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

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

Модель NQ:

N x Q > 10000

где,

N = количество элементов данных

Q = объем работы для каждого элемента

Ответ 4

Никогда не распараллеливайте бесконечный поток с ограничением. Вот что происходит:

    public static void main(String[] args) {
// let's count to 1 in parallel
System.out.println(
IntStream.iterate(0, i -> i + 1)
.parallel()
.skip(1)
.findFirst()
.getAsInt());
}

Результат

    Exception in thread "main" java.lang.OutOfMemoryError
at ...
at java.base/java.util.stream.IntPipeline.findFirst(IntPipeline.java:528)
at InfiniteTest.main(InfiniteTest.java:24)
Caused by: java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.stream.SpinedBuffer$OfInt.newArray(SpinedBuffer.java:750)
at ...

То же самое, если вы используете .limit(...)

Объяснение здесь: Java 8, использование .parallel в потоке вызывает ошибку ООМ

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

public static void main(String[] args) {
// let's count to 1 in parallel
System.out.println(
IntStream.range(1, 1000_000_000)
.parallel()
.skip(100)
.findFirst()
.getAsInt());
}

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

2023-03-18 16:47 java java-8 java-stream