ForkJoinPool останавливается во время invokeAll/join

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

Я этого не ожидал, так как строго старался использовать неблокирующие задачи. Мои наблюдения очень похожи на наблюдения ForkJoinPool, похоже, что-то напрасно. После большой отладки в ForkJoinPool у меня есть предположение:

Я использовал invokeAll() для распределения работы по списку подзадач. После того, как invokeAll() завершил выполнение первой задачи, он начинает присоединяться к другим. Это прекрасно работает до тех пор, пока следующая задача для присоединения не окажется поверх очереди выполнения. К сожалению, я представил дополнительные задачи асинхронно, не присоединяясь к ним. Я ожидал, что среда ForkJoin сначала продолжит выполнение этой задачи, а затем вернется к присоединению к оставшимся задачам.

Но, похоже, так не работает. Вместо этого рабочий поток останавливается, вызывая wait(), пока задача, ожидающая выполнения, не будет готова (предположительно, выполняется другим рабочим потоком). Я не проверял это, но, похоже, это общий недостаток вызова join ().

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

Итак, почему ForkJoinTask.doJoin() не просто выполняет любую доступную задачу поверх своей очереди, пока она не будет готова (либо выполнена сама, либо украдена другими)?

3 ответа

Поскольку никто, кажется, не понимает мой вопрос, я пытаюсь объяснить, что я нашел после нескольких ночей отладки:

Текущая реализация ForkJoinTasks работает хорошо, если все вызовы fork/join строго спарены. Иллюстрируя разветвление открывающей скобкой и соединяясь закрывающей, идеальный бинарный образец разветвления может выглядеть следующим образом:

{([] []) ([] [])} {([] []) ([] [])}

Если вы используете invokeAll(), вы также можете отправить список подзадач, например:

{([] [] [] []) ([] [] [] []) ([] [] [] [])}

Однако то, что я сделал, выглядит следующим образом:

{([) ([)}...]]

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

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

Недостатком, по-видимому, является текущая реализация join (): присоединение к an ) ожидает увидеть соответствующую ему ( поверх своей очереди выполнения), но находит [ и озадачен. Вместо того, чтобы просто выполнить [], чтобы избавиться от него, текущий поток приостанавливается (вызывая wait ()) до тех пор, пока кто-то другой не придет выполнить неожиданную задачу, что приведет к резкому снижению производительности.

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

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

Как я там сказал, фреймворк не может выполнять вновь отправленные запросы, пока не завершит предыдущие. И каждый WorkThread не может украсть, пока не закончится текущий запрос, что приводит к ожиданию ().

Дополнительные темы, которые вы видите, являются "потоками продолжения". Так как join () в конечном итоге выдает wait(), эти потоки необходимы, чтобы весь фреймворк не зависал.

Вы не используете эту платформу для очень узкой цели, для которой она была предназначена.

Фреймворк начал жизнь как эксперимент в исследовательской работе 2000 года. С тех пор он был изменен, но базовый дизайн, объединяющий большие массивы, остается прежним. Основная цель - научить магистрантов ходить по листьям сбалансированного дерева. Когда люди используют его не для простой обработки массива, происходят странные вещи. То, что он делает в Java7, мне недоступно; что является целью статьи.

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

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

Другие вопросы по тегам