Спеціальний пул потоків у паралельному потоці Java 8


398

Чи можна вказати спеціальний пул потоків для паралельного потоку Java 8 ? Я не можу його знайти ніде.

Уявіть, що у мене є серверна програма, і я хотів би використовувати паралельні потоки. Але додаток великий і багатопотоковий, тому я хочу його розділити. Я не хочу повільно виконуваного завдання в одному модулі завдань блокування додатків з іншого модуля.

Якщо я не можу використовувати різні пули потоків для різних модулів, це означає, що я не можу безпечно використовувати паралельні потоки в більшості ситуацій реального світу.

Спробуйте наступний приклад. Існують деякі інтенсивні завдання процесора, виконані в окремих потоках. Завдання використовують паралельні потоки. Перше завдання порушено, тому кожен крок займає 1 секунду (імітується режимом сну). Проблема полягає в тому, що інші нитки застрягають і чекають, коли зламане завдання закінчиться. Це надуманий приклад, але уявіть додаток сервлетів і хтось, хто подає тривале завдання в пул приєднання до спільної вилки.

public class ParallelTest {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService es = Executors.newCachedThreadPool();

        es.execute(() -> runTask(1000)); //incorrect task
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));


        es.shutdown();
        es.awaitTermination(60, TimeUnit.SECONDS);
    }

    private static void runTask(int delay) {
        range(1, 1_000_000).parallel().filter(ParallelTest::isPrime).peek(i -> Utils.sleep(delay)).max()
                .ifPresent(max -> System.out.println(Thread.currentThread() + " " + max));
    }

    public static boolean isPrime(long n) {
        return n > 1 && rangeClosed(2, (long) sqrt(n)).noneMatch(divisor -> n % divisor == 0);
    }
}

3
Що ви маєте на увазі під власним пулом потоків? Існує один загальний ForkJoinPool, але ви завжди можете створити свій власний ForkJoinPool і подати до нього запити.
— оприлюднений

7
Підказка: Чемпіон Java Хайнц Кабуц вивчає ту саму проблему, але з ще гіршим впливом. Дивіться javaspecialists.eu/archive/Issue223.html
— Peti

Відповіді:


395

Насправді є хитрість, як виконати паралельну операцію в конкретному пулі fork-join. Якщо ви виконаєте його як завдання в пулі fork-join, він залишається там і не використовує загальний.

final int parallelism = 4;
ForkJoinPool forkJoinPool = null;
try {
    forkJoinPool = new ForkJoinPool(parallelism);
    final List<Integer> primes = forkJoinPool.submit(() ->
        // Parallel task here, for example
        IntStream.range(1, 1_000_000).parallel()
                .filter(PrimesPrint::isPrime)
                .boxed().collect(Collectors.toList())
    ).get();
    System.out.println(primes);
} catch (InterruptedException | ExecutionException e) {
    throw new RuntimeException(e);
} finally {
    if (forkJoinPool != null) {
        forkJoinPool.shutdown();
    }
}

Трюк заснований на ForkJoinTask.fork, який вказує: "Впорядковує асинхронно виконувати це завдання в пулі, в якому виконується поточне завдання, якщо це можливо, або використовуючи ForkJoinPool.commonPool (), якщо не inForkJoinPool ()"


20
Деталі щодо рішення описані тут blog.krecan.net/2014/03/18/…
— Лукас

3
Але чи вказано також, що потоки використовують ForkJoinPoolабо це деталі реалізації? Посилання на документацію було б непогано.
— Миколай

6
@Lukas Дякуємо за фрагмент Додам, що ForkJoinPoolекземпляр повинен бути, shutdown()коли він більше не потрібен, щоб уникнути протікання потоку. (приклад)
— jck

5
Зауважте, що в Java 8 є помилка, що хоч завдання виконуються у спеціальному екземплярі пулу, вони все ще поєднуються із спільним пулом: розмір обчислення залишається пропорційним загальному пулу, а не користувальницькому пулу. Виправлено на Java 10: JDK-8190974
— Terran

3
@terran Ця проблема також виправлена ​​для помилок
— Cutberto Ocampo

192

Паралельні потоки використовують за замовчуванням, ForkJoinPool.commonPoolякий за замовчуванням має один менший потік, як у вас процесорів , як повертається Runtime.getRuntime().availableProcessors()(Це означає, що паралельні потоки використовують усі ваші процесори, оскільки вони також використовують основний потік):

Для додатків, які потребують окремого або спеціального пулу, ForkJoinPool може бути сконструйований із заданим рівнем цільового паралелізму; за замовчуванням дорівнює кількості доступних процесорів.

Це також означає, що якщо ви вклали паралельні потоки або декілька паралельних потоків, запущених одночасно, всі вони матимуть один і той же пул. Перевага: ви ніколи не будете використовувати більше за замовчуванням (кількість доступних процесорів). Недолік: ви можете не отримати "всіх процесорів", присвоєних кожному паралельному потоку, який ви ініціюєте (якщо у вас є більше одного). (Мабуть, ви можете використовувати ManagedBlocker, щоб обійти це.)

Щоб змінити спосіб виконання паралельних потоків, ви можете будь-який

  • надішліть виконання паралельного потоку до власного ForkJoinPool: yourFJP.submit(() -> stream.parallel().forEach(soSomething)).get();або
  • ви можете змінити розмір загального пулу, використовуючи системні властивості: System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20")для цільового паралелізму 20 потоків. Однак це більше не працює після підтримуваного патча https://bugs.openjdk.java.net/browse/JDK-8190974 .

Приклад останнього на моїй машині, яка має 8 процесорів. Якщо я запускаю таку програму:

long start = System.currentTimeMillis();
IntStream s = IntStream.range(0, 20);
//System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20");
s.parallel().forEach(i -> {
    try { Thread.sleep(100); } catch (Exception ignore) {}
    System.out.print((System.currentTimeMillis() - start) + " ");
});

Вихід:

215 216 216 216 216 216 216 216 315 316 316 316 316 316 316 316 415 416 416 416

Таким чином, ви можете бачити, що паралельний потік обробляє 8 елементів одночасно, тобто він використовує 8 потоків. Однак якщо я коментую рядок, що коментується, вихід:

215 215 215 215 215 216 216 216 216 216 216 216 216 216 216 216 216 216 216 216

Цього разу паралельний потік використав 20 потоків, і всі 20 елементів у потоці були оброблені одночасно.


30
commonPoolМає фактично один менше availableProcessors, що призводить до повної паралельності дорівнювати , availableProcessorsтому що викликають підрахунки ниток , як один.
— Марко Топольник

2
подати декларацію ForkJoinTask. Для наслідування parallel() get()потрібно:stream.parallel().forEach(soSomething)).get();
— Григорій Кіслін

5
Я не впевнений, що ForkJoinPool.submit(() -> stream.forEach(...))запускатиму мої дії Stream із заданим ForkJoinPool. Я б очікував, що вся потокова дія буде виконана в ForJoinPool як ОДНА дія, але всередині все ще використовується стандартний / загальний ForkJoinPool. Де ви бачили, що ForkJoinPool.submit () буде робити те, що ви говорите?
— Фредерік Лейтенбергер

@FredericLeitenberger Ви, мабуть, мали намір розмістити свій коментар під відповіддю Лукаша.
— assylias

2
Я бачу, зараз stackoverflow.com/a/34930831/1520422 добре показує, що він насправді працює як оголошено. Але я все ще не розумію, як це працює. Але я добре з "це працює". Дякую!
— Фредерік Лейтенбергер

39

Крім того, щоб випробувати паралельний обчислення всередині вашого forkJoinPool, ви також можете передати цей пул методу CompletableFuture.supplyAsync, як у:

ForkJoinPool forkJoinPool = new ForkJoinPool(2);
CompletableFuture<List<Integer>> primes = CompletableFuture.supplyAsync(() ->
    //parallel task here, for example
    range(1, 1_000_000).parallel().filter(PrimesPrint::isPrime).collect(toList()), 
    forkJoinPool
);

22

Оригінальне рішення (встановлення загальної властивості паралелізму ForkJoinPool) більше не працює. Переглядаючи посилання в оригінальній відповіді, оновлення, яке порушує це, було перенесено на Java 8. Як уже згадувалося у пов'язаних потоках, це рішення не гарантується, що воно працює назавжди. Виходячи з цього, рішення - це forkjoinpool.submit з .get рішенням, обговореним у прийнятій відповіді. Думаю, що резервний портфель також фіксує ненадійність цього рішення.

ForkJoinPool fjpool = new ForkJoinPool(10);
System.out.println("stream.parallel");
IntStream range = IntStream.range(0, 20);
fjpool.submit(() -> range.parallel()
        .forEach((int theInt) ->
        {
            try { Thread.sleep(100); } catch (Exception ignore) {}
            System.out.println(Thread.currentThread().getName() + " -- " + theInt);
        })).get();
System.out.println("list.parallelStream");
int [] array = IntStream.range(0, 20).toArray();
List<Integer> list = new ArrayList<>();
for (int theInt: array)
{
    list.add(theInt);
}
fjpool.submit(() -> list.parallelStream()
        .forEach((theInt) ->
        {
            try { Thread.sleep(100); } catch (Exception ignore) {}
            System.out.println(Thread.currentThread().getName() + " -- " + theInt);
        })).get();

Я не бачу змін паралелізму, коли я ForkJoinPool.commonPool().getParallelism()в режимі налагодження.
— d-coder

Дякую. Я зробив кілька тестувань / досліджень і оновив відповідь. Схоже, оновлення змінило його, як це працює в старих версіях.
— Tod Casasent

Чому я продовжую отримувати це unreported exception InterruptedException; must be caught or declared to be thrownнавіть з усіма catchвинятками в циклі.
— Rocky Li

Роккі, я не бачу помилок. Знання версії Java та точна лінія допоможуть. "Перервана ексцепція" пропонує спробувати не перекрити сон у вашій версії належним чином.
— Tod Casasent

13

Ми можемо змінити паралелізм за замовчуванням, використовуючи таке властивість:

-Djava.util.concurrent.ForkJoinPool.common.parallelism=16

який може налаштувати використовувати більше паралелізму.


Хоча це глобальне налаштування, воно працює для збільшення
— paralStream

Це працювало для мене у версії openjdk "1.8.0_222"
— abbas

Та сама людина, що і вище, це не працює для мене на openjdk "11.0.6"
— abbas

8

Для вимірювання фактичної кількості використовуваних ниток можна перевірити Thread.activeCount():

    Runnable r = () -> IntStream
            .range(-42, +42)
            .parallel()
            .map(i -> Thread.activeCount())
            .max()
            .ifPresent(System.out::println);

    ForkJoinPool.commonPool().submit(r).join();
    new ForkJoinPool(42).submit(r).join();

Це може створити на 4-ядерному процесорі такий вихід, як:

5 // common pool
23 // custom pool

Без .parallel()цього дає:

3 // common pool
4 // custom pool

6
Thread.activeCount () не вказує вам, які потоки обробляють ваш потік. Зобразити на Thread.currentThread (). GetName (), а потім різний (). Тоді ви зрозумієте, що не кожен потік у пулі буде використаний ... Додайте затримку до вашої обробки і всі потоки в пулі будуть використані.
— keyoxy

7

До цього часу я використовував рішення, описані у відповідях на це питання. Тепер я створив невелику бібліотеку під назвою Підтримка паралельних потоків для цього:

ForkJoinPool pool = new ForkJoinPool(NR_OF_THREADS);
ParallelIntStreamSupport.range(1, 1_000_000, pool)
    .filter(PrimesPrint::isPrime)
    .collect(toList())

Але, як @PabloMatiasGomez зазначив у коментарях, є недоліки щодо механізму розщеплення паралельних потоків, що сильно залежить від розміру загального пулу. Дивіться, що паралельний потік з HashSet не працює паралельно .

Я використовую це рішення лише для того, щоб мати окремі пули для різних видів роботи, але я не можу встановити розмір загального пулу до 1, навіть якщо я не використовую його.


4

Примітка. Здається, що в JDK 10 є виправлення, що гарантує, що користувальницький пул потоків використовує очікувану кількість потоків.

Виконання паралельного потоку в користувацькому ForkJoinPool має підкорятися паралельності https://bugs.openjdk.java.net/browse/JDK-8190974


1

Я спробував користувальницький ForkJoinPool, щоб змінити розмір пулу:

private static Set<String> ThreadNameSet = new HashSet<>();
private static Callable<Long> getSum() {
    List<Long> aList = LongStream.rangeClosed(0, 10_000_000).boxed().collect(Collectors.toList());
    return () -> aList.parallelStream()
            .peek((i) -> {
                String threadName = Thread.currentThread().getName();
                ThreadNameSet.add(threadName);
            })
            .reduce(0L, Long::sum);
}

private static void testForkJoinPool() {
    final int parallelism = 10;

    ForkJoinPool forkJoinPool = null;
    Long result = 0L;
    try {
        forkJoinPool = new ForkJoinPool(parallelism);
        result = forkJoinPool.submit(getSum()).get(); //this makes it an overall blocking call

    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    } finally {
        if (forkJoinPool != null) {
            forkJoinPool.shutdown(); //always remember to shutdown the pool
        }
    }
    out.println(result);
    out.println(ThreadNameSet);
}

Ось висновок, який говорить, що пул використовує більше потоків, ніж за замовчуванням 4 .

50000005000000
[ForkJoinPool-1-worker-8, ForkJoinPool-1-worker-9, ForkJoinPool-1-worker-6, ForkJoinPool-1-worker-11, ForkJoinPool-1-worker-10, ForkJoinPool-1-worker-1, ForkJoinPool-1-worker-15, ForkJoinPool-1-worker-13, ForkJoinPool-1-worker-4, ForkJoinPool-1-worker-2]

Але насправді є дивацтво , коли я намагався досягти такого ж результату, використовуючи ThreadPoolExecutorнаступне:

BlockingDeque blockingDeque = new LinkedBlockingDeque(1000);
ThreadPoolExecutor fixedSizePool = new ThreadPoolExecutor(10, 20, 60, TimeUnit.SECONDS, blockingDeque, new MyThreadFactory("my-thread"));

але я провалився.

Це лише розпочнеться paralStream у новій темі, а потім все інше точно так само, що ще раз доводить, що parallelStreamволя використовуватиме ForkJoinPool для запуску дочірніх потоків.


Що може бути причиною того, що не допускати інших виконавців?
— омего

@omjego Це гарне запитання, можливо, ви можете почати нове запитання та надати більше деталей для розробки своїх ідей;)
— Hearen

1

Ідіть, щоб отримати AbacusUtil . Номер потоку можна вказати для паралельного потоку. Ось зразок коду:

LongStream.range(4, 1_000_000).parallel(threadNum)...

Розкриття: Я розробник AbacusUtil.


1

Якщо ви не хочете покладатися на хаки для впровадження, завжди є спосіб досягти цього, застосовуючи власні колекціонери, які поєднують mapі collectсемантику ... і ви не обмежуєтеся ForkJoinPool:

list.stream()
  .collect(parallelToList(i -> fetchFromDb(i), executor))
  .join()

На щастя, це вже зроблено тут і доступно на Maven Central: http://github.com/pivovarit/parallel-collectors

Відмова: Я написав це і несу відповідальність за це.


0

Якщо ви не заперечуєте проти використання сторонньої бібліотеки, за допомогою циклоп-реагування ви можете змішувати послідовні та паралельні потоки в межах одного трубопроводу та надавати власні ForkJoinPools. Наприклад

 ReactiveSeq.range(1, 1_000_000)
            .foldParallel(new ForkJoinPool(10),
                          s->s.filter(i->true)
                              .peek(i->System.out.println("Thread " + Thread.currentThread().getId()))
                              .max(Comparator.naturalOrder()));

Або якщо ми хотіли б продовжувати обробку в послідовному потоці

 ReactiveSeq.range(1, 1_000_000)
            .parallel(new ForkJoinPool(10),
                      s->s.filter(i->true)
                          .peek(i->System.out.println("Thread " + Thread.currentThread().getId())))
            .map(this::processSequentially)
            .forEach(System.out::println);

[Розкриття інформації Я є провідним розробником циклоп-реакції]


0

Якщо вам не потрібен користувальницький ThreadPool, але ви хочете обмежити кількість одночасних завдань, ви можете використовувати:

List<Path> paths = List.of("/path/file1.csv", "/path/file2.csv", "/path/file3.csv").stream().map(e -> Paths.get(e)).collect(toList());
List<List<Path>> partitions = Lists.partition(paths, 4); // Guava method

partitions.forEach(group -> group.parallelStream().forEach(csvFilePath -> {
       // do your processing   
}));

(Дублікат із запитанням про це заблоковано, тому, будь ласка, перенесіть мене тут)


-2

ви можете спробувати реалізувати цей ForkJoinWorkerThreadFactory та ввести його до класу Fork-Join.

public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode) {
        this(checkParallelism(parallelism),
             checkFactory(factory),
             handler,
             asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
             "ForkJoinPool-" + nextPoolId() + "-worker-");
        checkPermission();
    }

ви можете використовувати цей конструктор пулу Fork-Join для цього.

Примітки: 1. Якщо ви користуєтесь цим, врахуйте, що на основі вашої реалізації нових потоків буде впливати планування з JVM, яке, як правило, планує потоки приєднання вилок до різних ядер (трактуються як обчислювальна нитка). 2. Планування завдань за допомогою fork-join до потоків не вплине. 3. Не справді зрозумів, як паралельний потік вибирає нитки з fork-join (не вдалося знайти належну документацію на нього), тож спробуйте скористатися іншою фабрикою потоку Naming, щоб переконатися, чи збираються нитки в паралельному потоці від customThreadFactory, який ви надаєте. 4. commonThreadPool не використовуватиме цей customThreadFactory.


Чи можете ви надати корисний приклад, який би демонстрував, як використовувати те, що ви вказали?
— Дж. Мюррей
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.