Используйте CompletableFuture на ForkJoinpool и избегайте ожидания потока

Привет, я думал, что с CompletableFuture и значением по умолчанию ForkJoinPool я смогу оптимизировать выполнение задачи больше, чем классическое ExecutorService, но я кое-что упустил

С этим кодом выполнение занимает 1 секунду, у меня есть 3 рабочих потока:

for (int i = 0; i < 3; i++) {
    final int counter = i;
    listTasks.add(CompletableFuture.supplyAsync(() -> {
        Thread.sleep(1000);
        System.out.println("Looking up " + counter + " on thread " + Thread.currentThread().getName());
        return null;
    }));
}

ОК, вроде нормально.

Но с этим кодом это занимает 3 секунды:

for (int i = 0; i < 9; i++) {
    final int counter = i;
    listTasks.add(CompletableFuture.supplyAsync(() -> {
        Thread.sleep(1000);
        System.out.println("Looking up " + counter + " on thread " + Thread.currentThread().getName());
        return null;
    }));
}

Я думал, что спящий поток будет использоваться для запуска другой ожидающей задачи, это также должно занять 1 секунду. Например, я читал, что состояние потока IO WAINTING означает, что поток можно повторно использовать для другой задачи. Могу ли я проверить это поведение с помощью Thread.sleep()? Мой метод тестирования неверен или я что-то неправильно понял?


person bodtx    schedule 26.08.2016    source источник
comment
Сколько процессоров у вас на компьютере?   -  person Andrew Lygin    schedule 26.08.2016
comment
Привет, 2 реальных, но я установил это, чтобы заставить 3 рабочих и избежать создания потоков: System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "3");   -  person bodtx    schedule 26.08.2016
comment
Очень хороший вопрос   -  person srk    schedule 14.06.2021


Ответы (2)


Спящий поток нельзя использовать для выполнения работы другого потока (особенно один поток не может спать для другого потока). Только ЦП может переключиться на второй поток, когда первый переходит в спящий режим.

Когда вы предоставляете задачу CompletableFuture.supplyAsync(), она переходит к экземпляру по умолчанию ForkJoinPool, у которого столько потоков, сколько процессоров на вашем компьютере. Вы вручную устанавливаете для трех выделенных потоков значение по умолчанию ForkJoinPool, поэтому ваши девять задач распределяются между ними поровну: каждый поток последовательно выполняет три задачи. Итак, у вас есть три секунды в результате.

person Andrew Lygin    schedule 26.08.2016
comment
Хорошо, спасибо за сон, но если я выполняю ввод-вывод, например, запись файла или вызов веб-службы вместо Thread.sleep, будет ли у меня тот же результат или будет ли поток ожидания повторно использоваться для запуска другого запроса ввода-вывода/веб-сайта? - person bodtx; 26.08.2016
comment
Если ваш поток зависнет на одну секунду, чтобы выполнить ввод-вывод, у вас будет абсолютно такой же эффект. - person Andrew Lygin; 26.08.2016
comment
В вашем случае у вас есть три потока, и они выбирают доступные задачи из общей очереди. Вы ставите девять задач в эту очередь. Каждый поток берет одну задачу, выполняет ее полностью (засыпает на одну секунду) и только потом берет другую задачу. Но процессор при этом (пока ваши потоки спят) имеет шанс переключиться на другие потоки (в том числе потоки других процессов) и выполнить какую-то полезную работу. - person Andrew Lygin; 26.08.2016
comment
Я думал, что есть некоторая оптимизация с неблокирующим вводом-выводом, например это - person bodtx; 26.08.2016
comment
Неблокирующий ввод-вывод (NIO) имеет другую архитектуру, чем пулы потоков, которые вы используете в своих фрагментах кода. Thread.sleep() и простой ввод-вывод не являются правильным выбором для имитации неблокирующих операций. Если бы вы использовали операции из java.nio, вы могли бы уменьшить время, но использование пулов потоков было бы бесполезно для этой задачи. - person Andrew Lygin; 26.08.2016
comment
С NIO ваш поток может отправить какую-то задачу ввода-вывода (например, записать буфер в файл) и немедленно продолжить работу, чтобы выполнить какую-то другую полезную работу (я думаю, это то, что вы назвали «повторным использованием для другой задачи»). Позже он может проверить результат операции ввода-вывода и решить, что делать дальше. В этом разница между обычными операциями ввода-вывода и NIO. - person Andrew Lygin; 26.08.2016

Давайте посчитаем. Если у вас есть 9 задач, и каждая задача приостанавливается на 1 секунду, и у вас есть 2 процессора, вы можете одновременно запускать только 2 приостановки по 1 секунде. Запустите это с 9 задачами, и вы получите не менее 3 секунд или не более 4 секунд.

Это не то же самое, что неблокирующий ввод-вывод, потому что этот исполняемый поток привязан к ЦП и не отдаст ЦП, пока не завершится (несмотря на спящий режим). Если вы посмотрите на такие вещи, как пул потоков ввода-вывода RxJava, он создает один поток для каждой задачи, что приемлемо для задач ввода-вывода (но не задач, связанных с процессором).

person John Vint    schedule 26.08.2016
comment
Согласитесь с математикой, если не было возможного переключения контекста. Но, как вы говорите, и как RxJava, похоже, делает с пулом потоков ввода-вывода, поток не может переключать контекст, даже если он заблокирован в вводе-выводе. ok это объясняет, как это работает с сервлетом, действительно нет никакого волшебного переключения контекста, только ThreadPool для заблокированного потока - person bodtx; 26.08.2016