Comments 11
Заодно замечу, что
Thread.sleep(1);
— довольно сомнительный способ имитации нагрузки, потому что собственно нагрузку на процессор он и не создаёт (распараллелится замечательно на сколько угодно отдельных нитей, вне зависимости от числа ядер процессора).Странно ожидать от статьи на которой висит тег «tutorial», что она не будет туториалом.
sleep
и занимает значительное количество времени. И пример, фактически, показывает, что 1 мс * array.length / количество_нитей_ForkJoinPool < 1 мс * array.length
. Ну… да, очевидно, что это так. Но пример слишком искусственный и немного скучноватый. Было бы интереснее, если бы там было что-то, где распараллеливание действительно выигрыш в реальной задаче даёт. Сортировку хотя бы.ForkJoinPool forkJoinPool = new ForkJoinPool();
System.out.println(forkJoinPool.invoke(counter));
Займет все ядра пока не доработает. Это часто является нежелательным поведением.
И на серверах так делать обычно не стоит.
Хорошая статья с точки зрения простоты объяснения.
Небольшое уточнение:
То есть при вызове метода fork() задача не «выполнила сама себя» магическим образом, а была выполнена в том же потоке из которого и был вызван данный метод.
В данном случае, да, но есть уточнение на счёт случая вызова из потока не относящегося ни к какому FJP:
Если смотреть в Джавадок метода fork(), то мы увидим, что он по возможности будет использовать текущий пул (пул, к которому "относится" вызывающий поток), но в противном случае будет использовать глобальный. Например, поведение с commonPool
можно было бы понаблюдать в Вашем примере с вызовом fork()
напрямую из main
.
Мелочь, просто чтобы не оставлять неочевидных моментов)
Ещё один момент:
Важно отметить, что метод fork() отправляет задачу в какой-либо поток, но при этом не запускает её выполнения.
На самом деле, fork()
отправляя задачу в поток на выполнение (пусть и без гарантии того, когда именно он ей займётся) и вызов join()
не обязателен для того, чтобы она была завершена, что можно увидеть на следующем синтетическом примере:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RecursiveAction;
class Scratch {
public static void main(String[] args) throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
new RecursiveAction() {
@Override
protected void compute() {
latch.countDown();
}
}.fork();
latch.await();
System.out.println("Completed without join()");
}
}
Последняя строчка успешно вызывается, что было бы невозможно, если бы задача не выполнилась.
Описана работа обычного ThreadPool - FJP решает проблемы, которые при обычном подходе возникают. Это не описано вообще. Принципиальное отличие в подходе WorkStealing, который часто подразумевает рекурсивные алгоритмы. WorkStealing - это не процесс вытаскивания задачи из общего пула, а процесс вытаскивания задачи из очереди другого обработчика, если (и только если) своя очередь пуста. Рекурсия здесь вот при чем - каждый обработчик разбивает задачу на подзадачи и помещает эти задачи в очередь, из очереди они попадают в отдельные очереди каждого обработчика (можно сделать без submission queue но не нужно). Это важно - в отличие от простой реализации пула потоков, очередь исполнения не одна, а отдельно для каждого потока. Это позволяет минимизировать синхронизацию обработчиков и именно ради этого все затевается. FJP спроектирован для мелких, нагружающих именно процессор задач, поэтому минимизация расходов на синхронизацию так важна и поэтому он по дефолту инициализируется количеством потоков равных числу вычислительных ядер.
Это не описано вообще. Принципиальное отличие в подходе WorkStealing
Вообще то описано. В статье:«ForkJoinPool – это пул потоков, преимущество которого состоит в том, что он работает на основе принципа WorkStealing»
WorkStealing — это не процесс вытаскивания задачи из общего пула, а процесс вытаскивания задачи из очереди другого обработчика
Здесь в целом согласен. Мой косяк. Спасибо, исправлю. Хотя, как это согласуется со словами: «помещает эти задачи в очередь, из очереди они попадают в отдельные очереди каждого обработчика» — не совсем понятно. То есть всё таки общая очередь задач есть.
каждый обработчик разбивает задачу на подзадачи— здесь просьба пояснить поподробнее, что имеется в виду. Как, на ваш взгляд, происходит разделение задачи на подзадачи?
Вилкой в глаз, или ForkJoinPool в Java