Правильное использование параллельных потоков в Java - PullRequest
0 голосов
/ 24 февраля 2019

Я экспериментирую с параллельными потоками в Java, и для этого у меня есть следующий код для вычисления числа простых чисел до n.

В основном у меня есть 2 метода

  • calNumberOfPrimes(long n) - 4 разных варианта
  • isPrime(long n) - 2 разных варианта

На самом деле у меня есть 2 разных варианта каждого из вышеуказанных методов, один вариант, которыйиспользует параллельные потоки и другой вариант, который не использует параллельные потоки.

    // itself uses parallel stream and calls parallel variant isPrime
    private static long calNumberOfPrimesPP(long n) {
        return LongStream
                .rangeClosed(2, n)
                .parallel()
                .filter(i -> isPrimeParallel(i))
                .count();
    }

    // itself uses parallel stream and calls non-parallel variant isPrime
    private static long calNumberOfPrimesPNP(long n) {
        return LongStream
                .rangeClosed(2, n)
                .parallel()
                .filter(i -> isPrimeNonParallel(i))
                .count();
    }

    // itself uses non-parallel stream and calls parallel variant isPrime
    private static long calNumberOfPrimesNPP(long n) {
        return LongStream
                .rangeClosed(2, n)
                .filter(i -> isPrimeParallel(i))
                .count();
    }

    // itself uses non-parallel stream and calls non-parallel variant isPrime
    private static long calNumberOfPrimesNPNP(long n) {
        return LongStream
                .rangeClosed(2, n)
                .filter(i -> isPrimeNonParallel(i))
                .count();
    }
    // uses parallel stream
    private static boolean isPrimeParallel(long n) {
        return LongStream
                .rangeClosed(2, (long) Math.sqrt(n))
                .parallel()
                .noneMatch(i -> n % i == 0);
    }

    // uses non-parallel stream
    private static boolean isPrimeNonParallel(long n) {
        return LongStream
                .rangeClosed(2, (long) Math.sqrt(n))
                .noneMatch(i -> n % i == 0);
    }

Я пытаюсь выяснить, какие из calNumberOfPrimesPP, calNumberOfPrimesPNP, calNumberOfPrimesNPP и calNumberOfPrimesNPNP являются лучшимис точки зрения правильного использования параллельных потоков с эффективностью и почему это лучше.

Я попытался рассчитать все эти 4 метода в 50 раз и взял среднее значение, используя следующий код:

    public static void main(String[] args) throws Exception {
        int iterations = 50;
        int n = 1000000;
        double pp, pnp, npp, npnp;
        pp = pnp = npp = npnp = 0;
        for (int i = 0; i < iterations; i++) {
            Callable<Long> runner1 = () -> calNumberOfPrimesPP(n);
            Callable<Long> runner2 = () -> calNumberOfPrimesPNP(n);
            Callable<Long> runner3 = () -> calNumberOfPrimesNPP(n);
            Callable<Long> runner4 = () -> calNumberOfPrimesNPNP(n);

            pp += TimeIt.timeIt(runner1);
            pnp += TimeIt.timeIt(runner2);
            npp += TimeIt.timeIt(runner3);
            npnp += TimeIt.timeIt(runner4);
        }
        System.out.println("___________final results___________");
        System.out.println("avg PP = " + pp / iterations);
        System.out.println("avg PNP = " + pnp / iterations);
        System.out.println("avg NPP = " + npp / iterations);
        System.out.println("avg NPNP = " + npnp / iterations);
    }

TimeIt.timeIt просто возвращает время выполнения в миллисекундах.Я получил следующий вывод:

___________final results___________
avg PP = 2364.51336366
avg PNP = 265.27284506
avg NPP = 11424.194316620002
avg NPNP = 1138.15516624

Теперь я пытаюсь рассуждать о вышеупомянутых временах выполнения:

  • Вариант PP не так быстр, как PNPвариант, потому что все параллельные потоки используют общий пул потоков fork-join и, если мы отправляем долгосрочную задачу, мы фактически блокируем все потоки в пуле.
  • Но приведенный выше аргумент также должен работать для варианта NPP, поэтому вариант NPP также должен быть примерно таким же быстрым, как вариант PNP.(Но это не тот случай, вариант NPP является худшим с точки зрения затраченного времени).Кто-нибудь может объяснить, пожалуйста, причину этого?

Мои вопросы:

  • Верны ли мои рассуждения относительно малого времени исполнения варианта PNP?
  • Я что-то упустил?
  • Почему NPP вариант является худшим (с точки зрения времени работы)?

Как TimeIt измеряет время:

class TimeIt {
    private TimeIt() {
    }

    /**
     * returns the time to execute the Callable in milliseconds
     */
    public static <T> double timeIt(Callable<T> callable) throws Exception {
        long start = System.nanoTime();
        System.out.println(callable.call());
        return (System.nanoTime() - start) / 1.0e6;
    }
}

PS: я понимаю, что это не лучший метод для подсчета количества простых чисел. Сито Эратосфена и другие более сложные методы существуют для этого.Но на этом примере я просто хочу понять поведение параллельных потоков и когда их использовать.

1 Ответ

0 голосов
/ 24 февраля 2019

Я думаю, понятно, почему АЭС такая медленная.

Упорядочите полученные цифры в таблице:

       |    _P    |   _NP
-------+----------+---------
  P_   |   2364   |   265
-------+----------+---------
  NP_  |  11424   |  1138
-------+----------+---------

Итак, вы видите, что всегда быстрее, когда внешняяпоток параллельныйЭто потому, что в потоке еще много работы.Таким образом, дополнительные затраты на обработку параллельного потока невелики по сравнению с выполняемой работой.

Вы также видите, что всегда быстрее, когда внутренний поток не параллелен.isPrimeNonParallel быстрее, чем isPrimeParallel.Это потому, что в потоке не так много работы.В большинстве случаев после нескольких шагов становится ясно, что число не простое.Половина чисел четная (только один шаг).Дополнительные издержки на обработку параллельного потока высоки по сравнению с работой, которую предстоит выполнить.

...