Более 2 потоков работают медленнее, чем 1 или 2 потока, если Thread.sleep(1) не помещен в метод run() потока

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

Однако один поток всегда быстрее, независимо от того, выбираю ли я 2 потока (редактировать. 2 потока быстрее, но ненамного, а 4 потока медленнее, чем 1 поток, и я понятия не имею, почему. (Я мог бы даже сказать, что чем больше потоков тем медленнее становится). Надеюсь, кто-нибудь объяснит. Может я что-то не так делаю.

Ниже приведен мой код, который я написал до сих пор. Я использую ThreadPoolExecutor для выполнения задач (одна задача = одна последовательность Коллатца для одного числа в интервале).

Класс Коллатца:

    public class ParallelCollatz implements Runnable {
    private long result;
    private long inputNum;

    public long getResult() {
        return result;
    }
    public void setResult(long result) {
        this.result = result;
    }
    public long getInputNum() {
        return inputNum;
    }
    public void setInputNum(long inputNum) {
        this.inputNum = inputNum;
    }
    public void run() {

        //System.out.println("number:" + inputNum);
        //System.out.println("Thread:" + Thread.currentThread().getId());
        //int j=0;
        //if(Thread.currentThread().getId()==11) {
        //  ++j;
        //  System.out.println(j);
        //}

            long result = 1;

            //main recursive computation
            while (inputNum > 1) {

                if (inputNum % 2 == 0) {
                    inputNum = inputNum / 2;
                } else {
                    inputNum = inputNum * 3 + 1;
                }
                ++result;
            }
           // try {
                //Thread.sleep(10);
            //} catch (InterruptedException e) {
                // TODO Auto-generated catch block
        //      e.printStackTrace();
            //}
            this.result=result;
            return;
        }

}

И основной класс, где я запускаю потоки (да, пока я создаю два списка с одинаковыми номерами, так как после запуска с одним потоком начальные значения теряются):

        ThreadPoolExecutor executor = (ThreadPoolExecutor)Executors.newFixedThreadPool(1);
    ThreadPoolExecutor executor2 = (ThreadPoolExecutor)Executors.newFixedThreadPool(4);

    List<ParallelCollatz> tasks = new ArrayList<ParallelCollatz>();
    for(int i=1; i<=1000000; i++) {
        ParallelCollatz task = new ParallelCollatz();
        task.setInputNum((long)(i+1000000));
        tasks.add(task);

    }


    long startTime = System.nanoTime();
    for(int i=0; i<1000000; i++) {
        executor.execute(tasks.get(i));

    }

    executor.shutdown();
    boolean tempFirst=false;
    try {
        tempFirst =executor.awaitTermination(5, TimeUnit.HOURS);
    } catch (InterruptedException e1) {
        // TODO Auto-generated catch block
        e1.printStackTrace();
    }
    System.out.println("tempFirst " + tempFirst);
     long endTime = System.nanoTime();
    long    durationInNano = endTime - startTime;
    long    durationInMillis = TimeUnit.NANOSECONDS.toMillis(durationInNano);  //Total execution time in nano seconds
        System.out.println("laikas " +durationInMillis);


        List<ParallelCollatz> tasks2 = new ArrayList<ParallelCollatz>();
        for(int i=1; i<=1000000; i++) {
            ParallelCollatz task = new ParallelCollatz();
            task.setInputNum((long)(i+1000000));
            tasks2.add(task);

        }


        long startTime2 = System.nanoTime();
        for(int i=0; i<1000000; i++) {
            executor2.execute(tasks2.get(i));

        }

        executor2.shutdown();
        boolean temp =false;
        try {
             temp=executor2.awaitTermination(5, TimeUnit.HOURS);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("temp "+ temp);
         long endTime2 = System.nanoTime();
            long durationInNano2 = endTime2 - startTime2;
            long durationInMillis2 = TimeUnit.NANOSECONDS.toMillis(durationInNano2);  //Total execution time in nano seconds
            System.out.println("laikas2 " +durationInMillis2);

Например, работа с одним потоком завершается за 3280 мс. Запуск с двумя потоками 3437 мс. Должен ли я рассматривать другую параллельную структуру для вычисления каждого элемента?

ИЗМЕНИТЬ Пояснение. Я не пытаюсь распараллелить отдельные последовательности, а интервал чисел, когда каждое число имеет свою последовательность (что не связано с другими числами).

ИЗМЕНИТЬ2

Сегодня я запустил программу на хорошем ПК с 6 ядрами и 12 логическими процессорами, и проблема осталась. Кто-нибудь знает, где может быть проблема? Я также обновил свой код. 4 потока по какой-то причине работают хуже, чем 2 потока (даже хуже, чем 1 поток). Я также применил то, что было дано в ответе, но без изменений.

Еще одно редактирование Я заметил, что если я добавляю Thread.sleep(1) в свой метод ParallelCollatz, производительность постепенно увеличивается с увеличением количества потоков. Возможно, эта деталь подскажет кому-то, что не так? Однако независимо от того, сколько задач я даю, если нет Thread.Sleep(1), 2 потока работают быстрее всего, 1 поток находится на 2-м месте, а другие зависают примерно на такое же количество миллисекунд, но медленнее, чем 1 и 2 потока.

Новое редактирование Я также пробовал помещать больше задач (для цикла для вычисления не 1, а 10 или 100 последовательностей Коллатца) в методе run() класса Runnable, чтобы сам поток выполнял больше работы. К сожалению, это тоже не помогло. Может я неправильно запускаю задачи? У кого-нибудь есть идеи?

EDIT Таким образом, может показаться, что после добавления дополнительных задач в метод запуска это немного исправляется, но для большего количества потоков проблема все еще остается 8+. Мне все еще интересно, причина этого в том, что для создания и запуска потоков требуется больше времени, чем для выполнения задачи? Или мне создать новый пост с этим вопросом?


person Svajunas Kavaliauskas    schedule 27.04.2019    source источник
comment
Я вычисляю последовательность, скажем, для 10000 номеров. Каждое число имеет свою последовательность, которую я пытаюсь сделать параллельно, эти последовательности не связаны. Конечная цель будет состоять в том, чтобы найти самую длинную, но пока я просто пытаюсь запустить эти последовательности в отдельных потоках.   -  person Svajunas Kavaliauskas    schedule 27.04.2019
comment
ты используешь 1 ядро?   -  person aran    schedule 27.04.2019
comment
Я использую ноутбук с процессором Intel Core i5-4202Y (не самый лучший, но он имеет 2 ядра и 4 логических процессора).   -  person Svajunas Kavaliauskas    schedule 27.04.2019
comment
Попробуйте максимизировать тест, чтобы он выполнялся 30 секунд вместо 3, и проверьте, использует ли он 2 логических процессора для задания. Если нет, то никакой оптимизации нет, и изменения очереди процессора могут вас задержать.   -  person aran    schedule 27.04.2019
comment
Это просто теория, похоже, это многозадачность, а не многопоточность.   -  person aran    schedule 28.04.2019
comment
Как проверить, сколько логических процессоров использует работающая программа. Кроме того, если это многозадачность, есть идеи, что может быть причиной? Насколько я проверил, добавив оператор печати в метод run(), есть 2 разных потока. (Это прокомментировано в опубликованном коде)   -  person Svajunas Kavaliauskas    schedule 28.04.2019
comment
А пока проверьте, нет ли разницы, или даже если однопоточное задание завершается раньше, чем многопоточное. Увеличьте его до 1 минуты или около того.   -  person aran    schedule 28.04.2019
comment
@aran После применения ответа ниже я получаю 40 с для одного потока и 30 с для двух потоков, однако 4 потока хуже с 42 с. Может быть потому что там всего 2 ядра?   -  person Svajunas Kavaliauskas    schedule 28.04.2019
comment
да, кажется правильным, хотя это может зависеть от многих факторов   -  person aran    schedule 28.04.2019
comment
@aran какие еще факторы могут быть?   -  person Svajunas Kavaliauskas    schedule 28.04.2019
comment
@aran Я пробовал работать на лучшем ПК с большим количеством ядер, но проблема остается той же.   -  person Svajunas Kavaliauskas    schedule 28.04.2019


Ответы (1)


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

executor.shutdown() не ждет завершения всех задач. После этого нужно позвонить executor.awaitTermination.

executor.shutdown();
executor.awaitTermination(5, TimeUnit.HOURS);

https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ExecutorService.html#shutdown()

Обновление Я считаю, что наша методология тестирования ошибочна. Я повторил ваш тест на своей машине (1 процессор, 2 ядра, 4 логических процессора), и время, измеренное от запуска к запуску, сильно отличалось.

Я считаю, что основными причинами являются следующие:

  • Время запуска JVM и JIT-компиляции. В начале код работает в интерпретируемом режиме.
  • результат расчета игнорируется. У меня нет интуиции, что удаляет JIT и что мы на самом деле измеряем.
  • линии печати в коде

Чтобы проверить это, я преобразовал ваш тест в JMH. Особенно:

  • Я преобразовал runnable в callable и возвращаю сумму результатов, чтобы предотвратить встраивание (в качестве альтернативы вы можете использовать BlackHole от JMH)
  • У моих задач нет состояния, я переместил все движущиеся части в локальные переменные. Сборщик мусора не требуется для очистки задач.
  • Я по-прежнему создаю исполнителей в каждом раунде. Это не идеально, но я решил оставить все как есть.

Результаты, которые я получил ниже, соответствуют моим ожиданиям: одно ядро ​​ожидает в основном потоке, работа выполняется на одном ядре, цифры примерно одинаковы.

Benchmark                  Mode  Cnt    Score    Error  Units
SpeedTest.multipleThreads  avgt   20  559.996 ± 20.181  ms/op
SpeedTest.singleThread     avgt   20  562.048 ± 16.418  ms/op

Обновленный код:

public class ParallelCollatz implements Callable<Long> {

    private final long inputNumInit;

    public ParallelCollatz(long inputNumInit) {
        this.inputNumInit = inputNumInit;
    }


    @Override
    public Long call() {
        long result = 1;
        long inputNum = inputNumInit;
        //main recursive computation
        while (inputNum > 1) {

            if (inputNum % 2 == 0) {
                inputNum = inputNum / 2;
            } else {
                inputNum = inputNum * 3 + 1;
            }
            ++result;
        }
        return result;
    }

}

и сам бенчмарк:

@State(Scope.Benchmark)
public class SpeedTest {
private static final int NUM_TASKS = 1000000;

    private static List<ParallelCollatz> tasks = buildTasks();

    @Benchmark
    @Fork(value = 1, warmups = 1)
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    @SuppressWarnings("unused")
    public long singleThread() throws Exception {
        ThreadPoolExecutor executorOneThread = (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
        return measureTasks(executorOneThread, tasks);
    }

    @Benchmark
    @Fork(value = 1, warmups = 1)
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    @SuppressWarnings("unused")
    public long multipleThreads() throws Exception {
        ThreadPoolExecutor executorMultipleThread = (ThreadPoolExecutor) Executors.newFixedThreadPool(4);
        return measureTasks(executorMultipleThread, tasks);
    }

    private static long measureTasks(ThreadPoolExecutor executor, List<ParallelCollatz> tasks) throws InterruptedException, ExecutionException {
        long sum = runTasksInExecutor(executor, tasks);
       return sum;
    }

    private static long runTasksInExecutor(ThreadPoolExecutor executor, List<ParallelCollatz> tasks) throws InterruptedException, ExecutionException {
        List<Future<Long>> futures = new ArrayList<>(NUM_TASKS);
        for (int i = 0; i < NUM_TASKS; i++) {
            Future<Long> f = executor.submit(tasks.get(i));
            futures.add(f);
        }
        executor.shutdown();

        boolean tempFirst = false;
        try {
            tempFirst = executor.awaitTermination(5, TimeUnit.HOURS);
        } catch (InterruptedException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
        long sum = 0l;
        for (Future<Long> f : futures) {
            sum += f.get();
        }
        //System.out.println(sum);
        return sum;
    }

    private static List<ParallelCollatz> buildTasks() {
        List<ParallelCollatz> tasks = new ArrayList<>();
        for (int i = 1; i <= NUM_TASKS; i++) {
            ParallelCollatz task = new ParallelCollatz((long) (i + NUM_TASKS));

            tasks.add(task);

        }
        return tasks;
    }

}
person Lesiak    schedule 27.04.2019
comment
Я сделал, как вы упомянули. Теперь я получаю 12 с и 19 с и еще медленнее. - person Svajunas Kavaliauskas; 28.04.2019
comment
У тебя всего 2 ядра. Я предполагаю, что один занят основным потоком, а 2 рабочих потока бьются на одном ядре. Я не знаю ни одного метода поиска текущего ядра непосредственно из Java, но чтобы получить некоторую интуицию, вы можете подключиться к своей программе с помощью jconsole/visualvm и наблюдать за своими потоками. Они работают одновременно без перебоев? - person Lesiak; 28.04.2019
comment
Я запускаю исполнитель с одним потоком, затем распечатываю время, затем запускаю исполнитель2 с двумя потоками и распечатываю время. Ничего не падает, и, насколько я вижу, 2 потока работают быстрее, по крайней мере, когда программа работает около 40 секунд. Но 4 потока медленнее, чем 1 поток. - person Svajunas Kavaliauskas; 28.04.2019
comment
Извините, но мы не на одной волне. Трэшинг (не сбой) означает, что несколько потоков конкурируют за один и тот же ресурс (в моей гипотезе — единственное оставшееся ядро) и одновременно выполняется только один, что означает никакого ускорения, только дополнительную работу по переключению контекста. Как работал эксперимент jconsole/visualvm? - person Lesiak; 28.04.2019
comment
О, извините, я невнимательно прочитал. Я никогда не использовал эти программы, поэтому может потребоваться некоторое время, чтобы попробовать их. Я прокомментирую, когда закончу. - person Svajunas Kavaliauskas; 28.04.2019
comment
Из того, что я вижу, с двумя потоками все работает нормально. Однако при запуске 3 или 4 потоков требуется много времени ожидания. Большую часть времени одновременно работает только один поток. (Я использую VisualVM). Что мне не нравится, так это то, что 2-й набор потоков, сколько бы их ни было, не заканчиваются в режиме отладки, почему-то программа просто зависает. - person Svajunas Kavaliauskas; 28.04.2019
comment
Я пробовал на более качественном ПК с большим количеством ядер, но проблема осталась прежней. Я понятия не имею, что я делаю неправильно. - person Svajunas Kavaliauskas; 28.04.2019
comment
Спасибо за подробный, обновленный ответ, после того, как вы попробовали разные вещи, кажется, что вычисление небольшого количества последовательностей Коллатца в потоке - не путь. Я получил ожидаемое ускорение, задав большее количество последовательностей для расчета потока (1000 или 10000). Конечно, я также запускал машину с большим количеством ядер. Еще раз спасибо за помощь. - person Svajunas Kavaliauskas; 06.05.2019