У паралельних нескінченних потоків Java не вистачає пам'яті


16

Я намагаюся зрозуміти, чому наведена нижче програма Java дає OutOfMemoryError, а відповідна програма без .parallel()цього не дає.

System.out.println(Stream
    .iterate(1, i -> i+1)
    .parallel()
    .flatMap(n -> Stream.iterate(n, i -> i+n))
    .mapToInt(Integer::intValue)
    .limit(100_000_000)
    .sum()
);

У мене є два питання:

  1. Який запланований вихід цієї програми?

    Без .parallel()цього здається, що це просто виводить, а sum(1+2+3+...)це означає, що він просто "застрягає" при першому потоці в flatMap, що має сенс.

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

  2. Що змушує у неї втрати пам’яті? Я спеціально намагаюся зрозуміти, як реалізуються ці потоки під кришкою.

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

Редагувати: Якщо це доречно, я використовую Java 11.

Editt 2: Мабуть, те ж саме відбувається навіть із простою програмою IntStream.iterate(1,i->i+1).limit(1000_000_000).parallel().sum(), тому це може мати відношення до ледачості, limitа не до того flatMap.


паралельно () внутрішньо використовує ForkJoinPool. Я здогадуюсь, що ForkJoin Framework знаходиться на Java з Java 7
aravind

Відповіді:


9

Ви кажете: " але я не зовсім знаю, в якому порядку оцінюються речі і де відбувається буферизація ", саме в цьому і полягають паралельні потоки. Порядок оцінювання не визначений.

Важливим аспектом вашого прикладу є .limit(100_000_000). Це означає, що реалізація не може просто підсумовувати довільні значення, а повинна підбивати перші 100 000 000 чисел. Зауважте, що в реалізації посилань .unordered().limit(100_000_000)результат не змінює результат, що вказує на відсутність спеціальної реалізації для не упорядкованого випадку, але це детальна інформація про реалізацію.

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

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

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

Коли ми сповільнюємо всі робочі потоки, крім однієї, що обробляє крайній лівий фрагмент, ми можемо зробити потік припиненим (принаймні в більшості пробігів):

System.out.println(IntStream
    .iterate(1, i -> i+1)
    .parallel()
    .peek(i -> { if(i != 1) LockSupport.parkNanos(1_000_000_000); })
    .flatMap(n -> IntStream.iterate(n, i -> i+n))
    .limit(100_000_000)
    .sum()
);

¹ Я дотримуюся пропозиції Стюарта Маркса використовувати замовлення зліва направо, коли йдеться про замовлення зустрічі, а не про порядок обробки.


Дуже приємна відповідь! Цікаво, чи існує навіть ризик, що всі потоки почнуть виконувати операції flatMap, і жоден не виділяється, щоб фактично спорожнити буфери (підсумовування)? У моєму фактичному випадку використання нескінченні потоки натомість файли занадто великі, щоб зберігати в пам'яті. Цікаво, як я можу переписати потік, щоб зменшити використання пам'яті?
Томас Ахле

1
Ви використовуєте Files.lines(…)? Він значно покращився на Java 9.
Холгер

1
Це робиться в Java 8. У нових JRE-файлах вона все одно повернеться до BufferedReader.lines()певних обставин (не файлова система за замовчуванням, спеціальна діаграма чи розмір більше Integer.MAX_FILES). Якщо застосовується одне із них, користувацьке рішення може допомогти. Цього варто було б отримати нові запитання і відповіді…
Holger

1
Integer.MAX_VALUEзвичайно…
Хольгер

1
Що таке зовнішній потік, потік файлів? Чи має він передбачуваний розмір?
Хольгер

5

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

Про OutOfMemoryErrorпомилку, яку ви отримуєте, повідомлялося в [JDK-8202307] Отримання java.lang.OutOfMemoryError: Куча Java простору при виклику Stream.iterator (). Next () у потоці, який використовує нескінченний / дуже великий потік у flatMap . Якщо ви подивитесь на квиток, ви отримаєте більш-менш той самий слід стека, який ви отримуєте. Квиток закрили, оскільки не виправлено з наступної причини:

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


Це дуже цікаво! Має сенс, що для переходу push / pull потрібна буферизація, яка може зайняти пам'ять. Однак у моєму випадку здається, що використання просто натискання має спрацювати нормально та просто відкидати решту елементів у міру їх появи? А може, ви говорите, що flapmap викликає створення ітератора?
Thomas Ahle

3

OOME спричиняється не нескінченним потоком, а тим, що це не так .

Тобто, якщо ви прокоментуєте це .limit(...), у нього ніколи не закінчиться пам'яті - але, звичайно, це ніколи не закінчиться.

Після розбиття потік може відслідковувати кількість елементів лише у тому випадку, якщо вони накопичені в межах кожного потоку (схоже, що це власне акумулятор Spliterators$ArraySpliterator#array).

Схоже, ви можете відтворити його без flatMap, просто запустіть наступне за допомогою -Xmx128m:

    System.out.println(Stream
            .iterate(1, i -> i + 1)
            .parallel()
      //    .flatMap(n -> Stream.iterate(n, i -> i+n))
            .mapToInt(Integer::intValue)
            .limit(100_000_000)
            .sum()
    );

Однак, прокоментувавши це limit(), він повинен працювати нормально, поки ви не вирішите пощадити свій ноутбук.

Окрім фактичних деталей щодо впровадження, ось що, на мою думку, відбувається:

З limit, sumредуктор хоче, щоб перші X елементи підсумовувались, тому жодна нитка не може видавати часткові суми. Кожен "фрагмент" (нитка) повинен буде накопичити елементи і пропустити їх. Без обмежень такого обмеження немає, тому кожен "фрагмент" просто обчислить часткову суму з елементів, які він отримує (назавжди), припускаючи, що він вийде в результаті.


Що ви маєте на увазі "раз він розколоться"? Чи обмежує це межа якось?
Thomas Ahle

@ThomasAhle parallel()використовуватиме ForkJoinPoolвнутрішньо для досягнення паралелізму. Заповіт Spliteratorбуде використовуватися для призначення роботи кожномуForkJoin задачі, я припускаю , що ми можемо назвати одиницю роботи тут , як «розкол».
Karol Dowbecki

Але чому це відбувається лише з обмеженням?
Thomas Ahle

@ThomasAhle я відредагував відповідь двома копійками.
Costi Ciudatu

1
@ThomasAhle встановив точку перерви в Integer.sum() , використовувану IntStream.sumредуктором. Ви побачите, що безлімітна версія закликає цю функцію весь час, тоді як обмежена версія ніколи не отримує дзвінок перед OOM.
Costi Ciudatu
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.