От чего зависит количество потоков, создаваемых Java ForkJoinPool?
==================================================================

Насколько я понял`ForkJoinPool`этот пул создает фиксированное количество потоков (по умолчанию: количество ядер) и никогда не будет создавать больше потоков (если приложение не указывает на необходимость в них с помощью`managedBlock`). 

Однако, используя`ForkJoinPool.getPoolSize()` Я обнаружил, что в программе, которая создает 30 000 задач (`RecursiveAction`), то`ForkJoinPool` при выполнении этих задач в среднем используется 700 потоков (число потоков учитывается при каждом создании задачи). Задачи не выполняют ввод-вывод, а просто вычисляют; единственная межзадачная синхронизация вызывает`ForkJoinTask.join()` и доступ`AtomicBoolean`s, то есть нет операций по блокировке потоков.

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

Итак, почему`ForkJoinPool` создать столько потоков? Какие факторы определяют количество созданных потоков?

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

Программа ищет в лабиринте путь от заданной начальной точки до заданной конечной точки, используя поиск в глубину. Решение гарантированно существует. Основная логика в`compute()` метод`SolverTask`: A`RecursiveAction` который начинается в некоторой заданной точке и продолжается со всеми соседними точками, достижимыми из текущей точки. Вместо того, чтобы создавать новый`SolverTask` в каждой точке ветвления (которая создала бы слишком много задач) он выталкивает всех соседей, кроме одного, в стек обратного отслеживания для последующей обработки и продолжает работу только с одним соседом, не помещенным в стек. Как только он таким образом достигает тупика, точка, недавно добавленная к стеку обратного отслеживания, отбрасывается, и поиск продолжается оттуда (соответственно сокращая путь, построенный из начальной точки taks). Новая задача создается, когда задача обнаруживает, что ее стек отслеживания больше определенного порога; с этого времени задача, продолжая извлекаться из своего стека обратного отслеживания до тех пор, пока она не будет исчерпана, не будет выдвигать дальнейшие точки в свой стек при достижении точки ветвления, но будет создавать новую задачу для каждой такой точки. Таким образом, размер задач может быть скорректирован с использованием порогового предела стека.

Числа, которые я цитировал выше («30 000 задач, в среднем 700 потоков») взяты из поиска в лабиринте 5000x5000 ячеек. Итак, вот основной код:

 

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

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



 полный и конечный имеет отношение к обработке ориентированного ациклического графа (DAG). Вы можете погуглить эти термины, чтобы получить полное представление о них. Это тип обработки, который был разработан для обработки. Посмотрите на код в API для Recursive ..., фреймворк использует ваш код compute () для создания других ссылок compute (), а затем выполняет join (). Каждая задача выполняет одно соединение (), как при обработке группы DAG.

 Вы не выполняете обработку DAG. Вы создаете много новых задач и ждете (join ()) для каждой. Прочитайте в исходном коде. Это ужасно сложно, но вы можете понять это. Фреймворк не делает правильного управления задачами. Куда он помещает ожидающую задачу, когда выполняет join ()? Нет приостановленной очереди, которая потребовала бы, чтобы поток монитора постоянно просматривал очередь, чтобы увидеть, что закончено. Вот почему фреймворк использует «потоки продолжения». Когда одна задача действительно присоединяется (), фреймворк предполагает, что она ожидает завершения одной более низкой Задачи. Когда присутствует много методов join (), поток не может продолжаться, поэтому должен существовать вспомогательный поток или поток продолжения.

 Как отмечалось выше, вам нужен процесс fork-join типа scatter-collect. Там вы можете раскошелиться как можно больше зада



  [Холгер Пейн](https://stackoverflow.com/users/1208965/holger-peine) а также [Неуловимый-код](https://stackoverflow.com/users/3016088/elusive-code) на самом деле не следует рекомендуемой практике, которая появилась в[javadoc для версии 1.8](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinTask.html):

 В наиболее типичных случаях пара fork-join действует как вызов (fork) и return (join) из параллельной рекурсивной функции. Как и в случае с другими формами рекурсивных вызовов, возврат (объединение) должен выполняться в первую очередь. Например,вилка(); b.fork (); b.join (); a.join ();, вероятно, будет значительно более эффективным, чем объединение кодаa перед кодомb.

 В обоих случаях FJPool создавался с помощью конструктора по умолчанию. Это приводит к строительству бассейна с AsyncMode = ложь, по умолчанию:

@ param asyncMode если true,  
establishes локальный режим планирования «первым пришел - первым вышел» для разветвленных задач, которые никогда не объединяются. Этот режим может быть более подходящим, чем режим по умолчанию для локального стека, в приложениях, в которых рабочие потоки обрабатывают только асинхронные задачи в стиле событий. Для значения по умолчанию используйте false.

 таким образом, рабочая очередь на самом деле пожизненная:  
head -> | t4 | T3 | t2 | t1 | ... | <- хвост

 Так что во фрагментах ониfork () все задачи складывают их в стек и чемприсоединиться( в том же порядке, то есть от самой глубокой задачи (t1) до самой верхней (t4), эффективно блокирующей, пока какой-нибудь другой поток не украдет (t1), затем (t2) и так далее. Поскольку существует множество задач для блокировки всех потоков пула (task\_count >> pool.getParallelism ()), компенсация запускается как [Луис Вассерман](https://stackoverflow.com/users/869736/louis-wasserman) описано.



 

[ForkJoinPool останавливается во время invokeAll / join](https://stackoverflow.com/questions/16894929/forkjoinpool-stalls-during-invokeall-join)

[ForkJoinPool, кажется, тратить нить](https://stackoverflow.com/questions/9677506/forkjoinpool-seems-to-waste-a-thread)

 Я сделал сокращенную версию того, что происходит (я использовал аргументы jvm: -Xms256m -Xmx1024m -Xss8m):

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

 Это случается в`java.util.concurrent.ForkJoinWorkerThread#joinTask`.

 Однако эта новая задача порождает больше таких же задач, но они не могут найти потоки в пуле, потому что потоки заблокированы в соединении. И поскольку он не может знать, сколько времени потребуется для их освобождения (поток может быть в бесконечном цикле или заблокирован навсегда), новый (-ие) поток (-ы) порождается (Компенсирует объединенные потоки как Луис Вассерман упомянуто):`java.util.concurrent.ForkJoinPool#signalWork`

 Чтобы предотвратить такой сценарий, нужно избегать рекурсивного нереста задач.

 Например, если в приведенном выше коде вы установили начальный параметр в 1, количество активных потоков будет равно 2, даже если вы увеличите childrenCount в десять раз.

 Также обратите внимание, что, хотя количество активных потоков увеличивается, количество запущенных потоков меньше или равно Параллелизм.