Логика Java fork / join framework
Это стало "побочным эффектом" к ответу на другой вопрос сегодня. Это скорее любопытство, чем реальная проблема.
Java SE 7 предлагает то, что Oracle называет "Fork / join framework". Это предположительно лучший способ планирования работы для нескольких процессоров. Хотя я понимаю, как это должно работать, я не могу понять ту часть, где это превосходство и претензии, сделанные о краже работы.Может быть, у кого-то другого есть больше понимания, почему это подход был бы желателен (кроме того, что он имеет причудливое название).
Базовыми примитивами fork/join являются ForkJoinTask
s, которые являются Future
s, и идея заключается в том, чтобылибо выполнить работу немедленно[sic] (формулировка вводит в заблуждение, поскольку "немедленно" подразумевает, что это происходит синхронно в главном потоке, в действительности это происходит внутри Future
) ниже определенного порога, либо разделить работу на две задачи рекурсивно, пока не будет достигнут порог.
Будущее-это концепция инкапсуляции задачи, которая выполняется асинхронно в объект непрозрачным и неопределенным способом. У вас есть функция, которая позволяет проверить, доступен ли результат, и вы получаете функцию, которая позволяет вам (ждать и) получить результат.
Строго говоря, вы даже не знаете, выполняется ли будущее асинхронно, оно может выполняться внутри get()
. Теоретически реализация может также породить поток для каждого будущего или использовать пул потоков.
На практике Java реализует фьючерсы как задачи в очереди задач с присоединенным пулом потоков (то же самое верно для всей платформы fork/join).
Документация fork/join дает этот конкретный пример использования:
protected void compute() {
if (mLength < sThreshold) {
computeDirectly();
return;
}
int split = mLength / 2;
invokeAll(new ForkBlur(mSource, mStart, split, mDestination),
new ForkBlur(mSource, mStart + split, mLength - split,
mDestination));
}
Это отправляет задачи в очередь задач базового threadpool способом, не зависящим от того, как Mergesort будет проходить их (благодаря рекурсии).
Скажем, например, что у нас есть массив из 32 "элементов" для обработки и имеет порог 4, и разделить равномерно, он будет производить 8 задания с 4 "предметами" в каждом, и выглядят так:
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
.
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15|16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
. . .
00 01 02 03 04 05 06 07|08 09 10 11 12 13 14 15|16 17 18 19 20 21 22 23|24 25 26 27 28 29 30 31
. . . . . . .
00 01 02 03|04 05 06 07|08 09 10 11|12 13 14 15|16 17 18 19|20 21 22 23|24 25 26 27|28 29 30 31
------------------------------------------------------------------------------------------------
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
На одноядерном процессоре это будет представлять / выполнять (очень сложным способом) группы задач 1-2-3-4-5-6-7-8 по порядку.
На двухъядерном процессоре это отправит / выполнит (1,3)-(2,4)-(5,7)-(6,8) [1].
На четырехъядерном процессоре это будет представлять / выполнять (1,3,5,7)-(2,4,6,8).
Для сравнения, наивная реализация без всей высшей магии просто поставила бы задачи 1-2-3-4-5-6-7-8 в очередь задач сразу же. Всегда.
На одноядерном процессоре это будет представлять / выполнять 1-2-3-4-5-6-7-8.
На двухъядерном процессоре это будет представлять / выполнять (1,2)-(3,4)-(5,6)-(7,8).
На четырехъядерном процессоре это будет представлять / выполнять (1,2,3,4)-(5,6,7,8).
Вопросы:
-
Вместо того чтобы просто запихиватьsThreshold последовательные элементы в одну задачу и отправлять одну задачу за другой в очередь задач пула потоков, a создается древовидная рекурсивная иерархия. Это включает в себя построение, обращение и уничтожение N + log2(N) объектов для N подзадач, которые фактически ничего не делают. Почему это превосходство?
-
Ни одна точка отсчета не сохраняется. Ни процессорные кэши, ни виртуальная память не любят, когда с ними так обращаются. Почему это превосходство?
-
За исключением однопроцессорной системы, задачи гарантированно не будут планироваться в порядке, близком к их первоначальному порядку. Это может быть не проблема, если это действительно не имеет значения, но это делает такие вещи, как, например, забор или барьер, в значительной степени неосуществимыми. Единственный способ иметь что-то вроде забора-это ждать завершения корневого объекта и только после этого отправлять новые задачи. Это эквивалентно полной остановке трубопровода (что именно то, чего вы никогда не хотите).
-
Документация Oracle утверждает, что этот подход реализует кражу работы и поэтому лучше, чем поток бассейн . Я не вижу, как это происходит. Все, что я вижу, - это очень сложный способ передачи задач в обычный пул потоков. Как это должно магически реализовывать кражу работы?
[1] давайте не будем делать это слишком сложным и предположим, что рабочие потоки не обгоняют друг друга, а все задачи обрабатываются в одно и то же время. В противном случае исполнение могло бы, конечно, происходить в другом порядке, хотя подчинение было бы тем же самым.
2 ответа:
Когда вы используете
ExecutorService
Вы будете решать, сколько потоков будет в пуле потоков, и нет никакого различия между задачами, которые Вы планируете, и подзадачами, которые эти задачи создают.ForkJoinPool
класс вместо этого управляет потоками на основе 1) доступных процессоров и 2) требований задачи.
В этом случае подзадачи, созданные активными задачами, планируются другими методами, нежели внешние задачи.
Мы, как правило, имеют одну вилку-присоединиться к бассейн для всего приложения (в отличие от использованияExecutorService
, где типично иметь более 1 в любом нетривиальном приложении) и нет необходимости вshutdown
.
Я не рассматривал внутренности, чтобы дать вам более низкое объяснение уровня, но если вы видите здесь есть представление и эталон, показывающий измерения, показывающие обещанный параллелизм.Обновление:
Эта структура решает определенные проблемы (ExecutorService
работает лучше для задач, которые есть сочетание активности процессора и ввода-вывода).
Основное мышление здесь, чтобы использовать рекурсивный / разделяй и властвуй подход, чтобы держать процессоры постоянно заняты. Идея состоит в том, чтобы создать новые задачи (разветвление) и приостановить текущую задачу до завершения новых задач (соединение), но Без создания новых потоков и Без наличия общей рабочей очереди.
Таким образом, Fork-join framework реализуется с использованием work-stealing путем создания ограниченного числа рабочих потоков(столько же, сколько ядер). Каждый рабочий поток поддерживает частную двустороннюю рабочую очередь.
При раздвоении рабочий выдвигает новую задачу во главе своего дека. Во время ожидания или простоя работник снимает задачу с головы своего дека и выполняет ее вместо сна.
Если дека рабочего пуста, крадет элемент из хвоста деки другого случайно выбранного рабочего.
Я бы рекомендовал прочитатьпараллелизм данных в Java , а также сделать некоторые тесты самостоятельно, чтобы убедиться. Теория хороша только до а точка. После этого сделайте свои измерения, чтобы увидеть, есть ли значительное преимущество производительности или нет
Позвольте мне начать со статьи [да, я написал ее], которая критикует рамки. A Java Fork-Join Calamity
Теперь к вашим вопросам:
Это не так. Фреймворк хочет обработать DAG. Такова структура дизайна.
Это не так. Как отмечается в статье, Java-приложения ничего не знают о кэшах, памяти и т. д. таким образом, предположения ошибочны.
- Да. Именно это и происходит. Ларьки настолько распространены, что рамки необходимо создать "потоки продолжения", чтобы продолжать двигаться. Статья ссылается на вопрос здесь, где более 700 потоков продолжения были необходимы.
Я, конечно, согласен, что код сложный. Разброс-сбор работает гораздо лучше, чем работа-Кража для приложений. Что касается документации, то какая документация? От Оракула нет никаких подробностей. Это все толчок, чтобы использовать рамки.
Существуют альтернативы.