Pull to refresh

Comments 11

Увидев заголовок «Вилкой в глаз», ожидал в тексте описание каких-то проблем (что-то в духе «как выстрелить себе в ногу»). Но нет, это просто tutorial.

Заодно замечу, что Thread.sleep(1); — довольно сомнительный способ имитации нагрузки, потому что собственно нагрузку на процессор он и не создаёт (распараллелится замечательно на сколько угодно отдельных нитей, вне зависимости от числа ядер процессора).
Я где то писал, что Thread.sleep(1) создаёт нагрузку на процессор? Идея в том, чтобы показать, что задача, которая при выполнении в один поток занимает значительное количество времени, при использовании ForkJoinPool будет выполнятся быстрее.
Странно ожидать от статьи на которой висит тег «tutorial», что она не будет туториалом.
Так в данном случае задача именно из-за наличия sleep и занимает значительное количество времени. И пример, фактически, показывает, что 1 мс * array.length / количество_нитей_ForkJoinPool < 1 мс * array.length. Ну… да, очевидно, что это так. Но пример слишком искусственный и немного скучноватый. Было бы интереснее, если бы там было что-то, где распараллеливание действительно выигрыш в реальной задаче даёт. Сортировку хотя бы.
Ваш код
ForkJoinPool forkJoinPool = new ForkJoinPool();
System.out.println(forkJoinPool.invoke(counter));

Займет все ядра пока не доработает. Это часто является нежелательным поведением.
И на серверах так делать обычно не стоит.
Спасибо за подсказку. Какое решение? Указать при создании ForkJoinPool количество используемых потоков?
Да, как минимум ограничить число потоков в fjp.

Еще лучше перейти на .parallelStream(). Его можно запустить в своем пуле потоков. Работает в общем случае аналогично.

Хорошая статья с точки зрения простоты объяснения.

Небольшое уточнение:

То есть при вызове метода 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 — это не процесс вытаскивания задачи из общего пула, а процесс вытаскивания задачи из очереди другого обработчика

Здесь в целом согласен. Мой косяк. Спасибо, исправлю. Хотя, как это согласуется со словами: «помещает эти задачи в очередь, из очереди они попадают в отдельные очереди каждого обработчика» — не совсем понятно. То есть всё таки общая очередь задач есть.
каждый обработчик разбивает задачу на подзадачи
— здесь просьба пояснить поподробнее, что имеется в виду. Как, на ваш взгляд, происходит разделение задачи на подзадачи?
Sign up to leave a comment.

Articles