Java ForkJoinPool с нерекурсивными задачами, работает ли воровство?

Я хочу представить Runnable задачи в ForkJoinPool с помощью метода:

forkJoinPool.submit(Runnable task)

Обратите внимание, я использую JDK 7.

Под капотом они превращаются в объекты ForkJoinTask. Я знаю, что ForkJoinPool эффективен, когда задача рекурсивно разбивается на более мелкие.

Вопрос:

Работает ли воровство в ForkJoinPool, если нет рекурсии?

Стоит ли это в этом случае?

Обновление 1: задачи небольшие и могут быть несбалансированными. Даже для строго одинаковых задач такие вещи, как переключение контекста, планирование потоков, парковка, пропуски страниц и т. Д., Мешают дисбалансу.

Обновление 2: Даг Ли написал в группе интересов Concurrency JSR-166, дав намек на это:

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

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

Обновление 3: Масштабируемость ForkJoinPool - бенчмаркинг команды пинг-понга Akka показывает отличные результаты.

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

1 ответ

Решение

ForkJoinPool В исходном коде есть хороший раздел под названием "Обзор реализации", который читайте для окончательной истины. Приведенное ниже объяснение - мое понимание JDK 8u40.

С первого дня, ForkJoinPool была рабочая очередь на рабочий поток (назовем их "рабочие очереди"). Разветвленные задачи помещаются в локальную рабочую очередь, готовые к повторной загрузке работником и выполнению - другими словами, это выглядит как стек с точки зрения рабочего потока. Когда работник истощает свою рабочую очередь, он обходит и пытается украсть задачи из других рабочих очередей. Это "кража работы".

Теперь, до (IIRC) JDK 7u12, ForkJoinPool была одна глобальная очередь отправки. Когда у рабочих потоков закончились локальные задачи, а также задачи для кражи, они попали туда и попытались проверить, доступна ли внешняя работа. В этом дизайне нет преимущества перед обычным, скажем, ThreadPoolExecutor при поддержке ArrayBlockingQueue,

Это значительно изменилось после этого. После того, как эта очередь представления была идентифицирована как серьезное узкое место производительности, Дуг Ли и соавт. чередуются очереди представления. Оглядываясь назад, это очевидная идея: вы можете повторно использовать большинство механизмов, доступных для рабочих очередей. Вы можете даже свободно распределить эти очереди на рабочие потоки. Теперь внешняя отправка попадает в одну из очередей отправки. Затем работники, которым нечем заняться, могут сначала заглянуть в очередь отправки, связанную с конкретным работником, а затем побродить, просматривая очереди отправки других. Это тоже можно назвать "воровством работы".

Я видел много рабочих нагрузок, извлекающих выгоду из этого. Это конкретное дизайнерское преимущество ForkJoinPool даже для простых нерекурсивных задач это было признано давно. Многие пользователи в службе concurrency-Interest@ просили простого исполнителя для кражи работы без ForkJoinPool arcanery. Это одна из причин, почему мы имеем Executors.newWorkStealingPool() в JDK 8 и далее - в настоящее время делегирование ForkJoinPool, но открыт для обеспечения более простой реализации.

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