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
, но открыт для обеспечения более простой реализации.