Search
Write a publication
Pull to refresh

Comments 7

Почему последняя картинка напомнила мне про 3 сентября? Крадетсо?

В sequential случае всё предельно просто — callsCounter замрёт на отметке 10. В случае parallel — как повезёт. Может 77, может 42.

Не нашёл отметки. Может, имелись в виду строки, которые в момент переноса кода на Хабр стёрлись?

Спасибо за замечание. Имеется ввиду, что счетчик в sequential случае после достижения значения 10 далее инкрментироваться не будет. В случае parallel stream сколько вызовов произойдет - не определено (не меньше 10 раз до ошибки счетчик будет инкрементирован, а дальше "как карта ляжет"). Изменил формулировку - должно стать понятнее.

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

Добавлю, что описанная проблема с callsCounter это не проблема parallel Stream как такового, а проблема исполнения задач в многопоточной среде.


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

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

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

Parallel stream отнюдь не инструмент для "многопоточности общего назначения". Он на мой субъективный взгляд идеологический продолжатель FJP и заточен на решение типовых задач подходящих для Fork Join (parallel map reduce например). Если многопотчным задачам нечего делать в FJP то и parallel stream не самый подходящий инструмент. Я бы сказал отнюдь не подходящий.

Про callsCounter это просто самый простой пример как показать, что при ошибках parallel stream может вернуть управление не дожидаясь завершения всех запущенных задач (в отличии от того же ExecutorService#invokeAll).

Извиняюсь, значит я неверно понял идею из текста непосредственно поста. Мне кажется, она недостаточно хорошо донесена.

Что такое “многопоточность общего назначения”?

Если задача выполняется несколькими потоками, то она уже многопоточна. А forkjojnpool это инструмент для помощи в реализации одного из способов достижения потокобезопасности, а именно разделение общего для всех обработчиков состояния таким образом, чтобы оно перестало быть общим. Например разделение задачи на части. Таким образом достигается потокобезопасность без использования специальнных механизмов синхронизации. Thread confinement если будет интересно почитать подробнее.

А в данной ситуации из-за введения общей для всех потоков переменной callsCounter идея ломается.

Понятно, что в таком случае forkjoinpool, вероятно, не самый подходящий вариант. Но имеем то что имеем.

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

Значит остается только верно синхронизировать работу с разделяемым состоянием.

Проблема возникает не столько из-за введения общей для всех переменной, сколько из-за её неправильного использования, так как в данной ситуации сначала происходит инкремент переменной, и только потом решение о том, стоит ли продолжать работу. Что некорректно.

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

Даже изменение очередности проверки состояния уже решает проблему. Отказываться от parallel stream нет необходимости.

Из выводов:

  1. Parallel stream или forkJoinPool придуманы для решения задач с разделением работы на части и выполнения частей паралельно.

  2. Введение общей переменной ломает идею, так как вводит разделяемое состояние и требует задуматься о более строгой синхронизации.

  3. Отказ от parallel stream (или fork join pool) не решит проблему, ведь проблема не в них самих, а в том, что синхронизация доступа к разделяемому состоянию выполнена некорректно.

Это то, что я ожидал бы в таком случае от статьи.

Под "многопоточностью общего назначения" я имею ввиду типичные варианты использования ExecutorService т.е. простых пулов потоков. FJP как ни крути предполагает work stealing. А если задачи не предполагают выигрыша от work stealing'а, то и смысла использовать FJP нет.

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

Sign up to leave a comment.

Articles