ForkJoinPool, кажется, тратить нить
Я сравниваю два варианта тестовой программы. Оба работают с 4-х потоков ForkJoinPool
на машине с четырьмя ядрами.
В "режиме 1" я использую пул очень похоже на службу исполнителя. Я бросаю кучу задач в ExecutorService.invokeAll
, Я получаю лучшую производительность, чем от обычной службы исполнителей с фиксированными потоками (хотя там и есть вызовы Lucene, которые выполняют некоторые операции ввода-вывода).
Здесь нет "разделяй и властвуй". Буквально я делаю
ExecutorService es = new ForkJoinPool(4);
es.invokeAll(collection_of_Callables);
В "режиме 2" я отправляю отдельную задачу в пул, и в этой задаче вызывается ForkJoinTask.invokeAll для отправки подзадач. Итак, у меня есть объект, который наследует от RecursiveAction
и передается в пул. В вычислительном методе этого класса я называю invokeAll
на коллекции объектов из другого класса, который также наследует от RecursiveAction
, В целях тестирования я отправляю только по одному из первых объектов. Что я наивно ожидал увидеть, что все четыре потока заняты, так как поток вызывает invokeAll
взял бы одну из подзадач для себя вместо того, чтобы просто сидеть и блокировать. Я могу подумать о некоторых причинах, почему это может не сработать.
При просмотре в VisualVM в режиме 2 один поток почти всегда ожидает. Я ожидаю увидеть поток, вызывающий invokeAll, который сразу же начнет работать над одной из вызванных задач, а не просто сидеть на месте. Это, безусловно, лучше, чем взаимоблокировки, которые могут возникнуть в результате использования этой схемы с обычным пулом потоков, но все же, что случилось? Он удерживает один поток на случай, если что-то еще будет отправлено? И, если так, то почему не та же проблема в режиме 1?
До сих пор я запускал это, используя jsr166 jar, добавленный в путь к загрузочному классу java 1.6.
2 ответа
ForkJoinTask.invokeAll разветвляется на все задачи, но первая в списке. Первая задача, которую он запускает сам. Тогда это присоединяется к другим задачам. Его поток никак не попадает в пул. Таким образом, вы видите то, что блокируете потоки для выполнения других задач.
Классическое использование invokeAll
для пула Fork Join - это разветвление одной задачи и вычисление другой (в этом выполняющемся потоке). Поток, который не разветвляется, присоединится после его вычисления. Воровство работы приходит с обеими задачами. Когда каждая задача вычисляется, ожидается, что она выполнит свои собственные подзадачи (пока не будет достигнут некоторый порог).
Я не уверен, что invokeAll вызывается для вашего RecursiveAction.compute()
но если это invokeAll, который принимает два RecursiveAction, он разветвляет одно, вычисляет другое и ожидает завершения разветвленной задачи.
Это отличается от простой службы executor, потому что каждая задача ExecutorService является просто Runnable в очереди. Нет необходимости для двух задач службы ExecutorService знать результат другой. Это основной случай использования пула FJ.