Чи слід завжди використовувати паралельний потік, коли це можливо?


514

З Java 8 та лямбдами легко переглядати колекції як потоки, а також використовувати паралельний потік так само просто. Два приклади з Документів , другий з використанням paralStream:

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

Поки я не дбаю про замовлення, чи завжди вигідно буде використовувати паралель? Можна було б подумати, що швидше розділити роботу на більше ядер.

Чи є інші міркування? Коли слід використовувати паралельний потік і коли слід використовувати паралельний потік?

(Це питання задається для того, щоб викликати дискусію про те, як і коли використовувати паралельні потоки, а не тому, що я думаю, що завжди використовувати їх - це гарна ідея.)

Відповіді:


735

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

  • Я обробляю величезну кількість предметів (або обробка кожного елемента займає час і є паралельною)

  • У мене в першу чергу проблема з продуктивністю

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

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

Крім того, пам’ятайте, що паралельні потоки не вирішують магічно всі проблеми синхронізації. Якщо спільний ресурс використовується предикатами та функціями, які використовуються в процесі, вам доведеться переконатися, що все захищено потоком. Зокрема, побічні ефекти - це речі, про які ви дійсно повинні турбуватися, якщо йдете паралельно.

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


18
Хороша відповідь. Я хотів би додати, що якщо у вас є величезна кількість елементів для обробки, це лише збільшує питання координації потоків; тільки паралелізація може бути корисною лише тоді, коли обробка кожного елемента потребує часу і паралелізація.
Warren Dew

16
@WarrenDew Я не згоден. Система Fork / Join просто розділить N елементів на, наприклад, 4 частини, і обробить ці 4 частини послідовно. 4 результати будуть зменшені. Якщо масив дійсно є масивним, навіть для швидкої обробки одиниць паралелізація може бути ефективною. Але, як завжди, доводиться міряти.
JB Nizet

У мене є колекція об'єктів, які реалізують, Runnableякі я закликаю start()використовувати їх як Threads, чи нормально це змінити на використання Java 8 потоків .forEach()паралельно? Тоді я зможу зняти код потоку з класу. Але чи є недоліки?
ycomp

1
@JBNizet Якщо 4 частини послідовно послідовно, то немає різниці, чи це процес паралелі чи послідовно знати? Pls
уточнюйте

3
@ Харшана він, очевидно, означає, що елементи кожної з 4 частин будуть оброблятися послідовно. Однак самі деталі можуть бути оброблені одночасно. Іншими словами, якщо у вас є кілька ядер процесора, кожна частина може працювати на своєму ядрі незалежно від інших частин, одночасно обробляючи власні елементи. (ПРИМІТКА. Я не знаю, якщо так працюють паралельні потоки Java, я просто намагаюся уточнити, що означав JBNizet.)
завтра

258

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

Однак, тому що це легко, не означає, що його завжди гарна ідея, а насправді погана ідея просто кинути .parallel()всюди просто тому, що ви можете.

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

Далі зазначимо, що паралелізм також часто виявляє недетермінізм у обчисленнях, які часто приховуються послідовними реалізаціями; іноді це не має значення, або їх можна пом'якшити обмеженням залучених операцій (тобто оператори скорочення повинні бути без громадянства та асоціації.)

Насправді іноді паралелізм пришвидшить ваше обчислення, іноді - не, а іноді навіть сповільнить його. Найкраще спершу розробитись із використанням послідовного виконання, а потім застосувати паралелізм де

(A) Ви знаєте, що насправді користь для підвищення продуктивності та

(B) що він фактично забезпечить підвищення продуктивності.

(A) - це бізнес-проблема, а не технічна. Якщо ви експерт з ефективності, зазвичай зможете переглянути код і визначити (B), але розумний шлях - це виміряти. (І навіть не турбуйтеся, поки не переконаєтесь у цьому (A); якщо код досить швидкий, краще застосуйте мозкові цикли в іншому місці.)

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

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


18
У цій публікації подано докладніші відомості про модель NQ: gee.cs.oswego.edu/dl/html/StreamParallelGuidance.html
Піно

4
@specializt: перемикання потоку від послідовного до паралельного робить зміна алгоритму (в більшості випадків). Згаданий тут детермінізм стосується властивостей, на які можуть покладатися (довільні) оператори (реалізація потоку цього не може знати), але, звичайно, не слід покладатися на них. Ось що намагався сказати той розділ цієї відповіді. Якщо ви піклуєтесь про правила, ви можете мати детермінований результат, як ви говорите, (інакше паралельні потоки були зовсім марними), але також існує можливість навмисно дозволеного недетермінізму, як, наприклад, при використанні findAnyзамість findFirst
Holger

4
"По-перше, зауважте, що паралелізм не пропонує жодних переваг, крім можливості більш швидкого виконання, коли доступно більше ядер" - або якщо ви застосовуєте дію, яка передбачає IO (наприклад myListOfURLs.stream().map((url) -> downloadPage(url))...).
Жуль

6
@Pacerier Це приємна теорія, але, на жаль, наївна (для початку див. 30-річну історію спроб побудови компіляторів автоматичного паралелізації). Оскільки не вдається здогадатися достатньо правильно часу, щоб не дратувати користувача, коли ми неминуче помиляємось, відповідальною справою було лише дозволити користувачеві сказати, що вони хочуть. У більшості ситуацій за замовчуванням (послідовним) є правильним і більш передбачуваним.
Брайан Гец

2
@Jules: Ніколи не використовуйте паралельні потоки для IO. Вони призначені виключно для інтенсивних операцій процесора. Паралельні потоки використовують, ForkJoinPool.commonPool()і ви не хочете, щоб завдання блокування переходили туди.
R2C2

68

Я дивився одну з презентацій з Брайан Гетц (Java Language Architect & специфікація свинцю для лямбда - виразів) . Він детально пояснює наступні 4 пункти, які слід враховувати, перш ніж йти на паралелізацію:

Витрати на розщеплення / розкладання
- Іноді розщеплення коштує дорожче, ніж просто виконувати роботу!
Витрати з відправлення / управління завданнями
- можуть зробити багато роботи за час, який потрібен для передачі роботи іншій нитці.
Витрати на комбінацію результатів
- іноді комбінація включає копіювання безлічі даних. Наприклад, додавання цифр є дешевим, тоді як об'єднання наборів дороге.
Місцевість
- слон в кімнаті. Це важливий момент, який може пропустити кожен. Ви повинні врахувати пропуски кешу, якщо процесор чекає даних через пропуски кешу, то ви нічого не отримаєте шляхом паралелізації. Ось чому джерела на основі масиву паралелізують найкраще, оскільки кешуються наступні індекси (біля поточного індексу), і є менше шансів, що процесор зазнає пропуску кешу.

Він також згадує відносно просту формулу, щоб визначити шанс паралельного прискорення.

Модель NQ :

N x Q > 10000

де,
N = кількість елементів даних
Q = обсяг роботи на предмет


13

JB вдарив цвяхом по голові. Єдине, що я можу додати, це те, що Java 8 не робить чисто паралельну обробку, вона робить паравіментальну . Так, я написав статтю, і займаюся F / J протягом тридцяти років, тому я розумію проблему.


10
Потоки не можна повторити, оскільки потоки роблять внутрішню ітерацію замість зовнішньої. У цьому вся причина в потоках. Якщо у вас є проблеми з навчальною роботою, то функціональне програмування може бути не для вас. Функціональне програмування === математика === академічне. І ні, J8-FJ не зламаний, просто більшість людей не читають посібник f ******. Документи Java говорять дуже ясно, що це не паралельна рамка виконання. У цьому вся причина для всіх матеріалів, що стосуються сплейтера. Так, це академічно, так, він працює, якщо ви знаєте, як ним користуватися. Так, користувальницьким виконавцем має бути простіше
Kr0e

1
У потоку є метод ітератора (), тож ви можете його повторити, якщо хочете. Я розумів, що вони не реалізують Iterable, тому що ви можете використовувати цей ітератор лише один раз, і ніхто не може вирішити, чи це добре.
Трейказ

14
якщо чесно: весь твій документ читається як масивний, витончений рент - і це в значній мірі заперечує його достовірність ... я б рекомендував повторно робити це з набагато менш агресивним підтекстом, інакше не багато людей насправді будуть намагатися повністю прочитати його. ... im just sayan
specializt

Кілька запитань щодо вашої статті ... Перш за все, чому ви, мабуть, порівнюєте збалансовані структури дерев із спрямовані ациклічні графіки? Так, збалансовані дерева - це DAG, але так вони пов'язані списками і майже всі об'єктно-орієнтовані структури даних, крім масивів. Крім того, якщо ви говорите, що рекурсивна декомпозиція працює лише на збалансованих структурах дерев і, отже, не має комерційного значення, як ви виправдовуєте це твердження? Мені здається (правда, без реального вивчення глибокого питання), що він повинен працювати так само добре, як на структурах даних на основі масиву, наприклад ArrayList/ HashMap.
Жуль

1
Цей потік з 2013 року, багато чого змінилося відтоді. Цей розділ призначений для коментарів, а не детальних відповідей.
оприлюднений

3

Інші відповіді вже охоплювали профілювання, щоб уникнути передчасної оптимізації та накладних витрат при паралельній обробці. Ця відповідь пояснює ідеальний вибір структур даних для паралельної трансляції.

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

Ще одним важливим фактором, що поєднує всі ці структури даних, є те, що вони забезпечують хорошу локацію посилань при послідовній обробці: послідовні посилання елементів зберігаються разом у пам'яті. Об'єкти, на які посилаються ці посилання, можуть не бути близькими один до одного в пам’яті, що зменшує локальність відліку. Локальність довідки виявляється критично важливою для паралелізації масових операцій: без цього потоки проводять більшу частину свого часу в режимі очікування, чекаючи передачі даних із пам'яті в кеш процесора. Структури даних з найкращою локальністю посилання є примітивними масивами, оскільки самі дані постійно зберігаються в пам'яті.

Джерело: Пункт № 48 Будьте обережні при створенні потоків паралельними, ефективними Java 3e Джошуа Блоха


2

Ніколи не паралелізуйте нескінченний потік з обмеженням. Ось що відбувається:

    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 у потоці викликає помилку OOM

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

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, через що це займе дуже багато часу.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.