Pool di thread personalizzati nel flusso parallelo Java 8


398

È possibile specificare un pool di thread personalizzato per il flusso parallelo Java 8 ? Non riesco a trovarlo da nessuna parte.

Immagina di avere un'applicazione server e vorrei utilizzare flussi paralleli. Ma l'applicazione è grande e multi-thread, quindi voglio compartimentarla. Non voglio un'attività in esecuzione lenta in un modulo delle attività di blocco dell'applicazione da un altro modulo.

Se non riesco a utilizzare pool di thread diversi per moduli diversi, significa che non posso utilizzare in sicurezza flussi paralleli nella maggior parte delle situazioni del mondo reale.

Prova il seguente esempio. Esistono alcune attività ad alta intensità di CPU eseguite in thread separati. Le attività sfruttano flussi paralleli. La prima attività viene interrotta, quindi ogni passaggio richiede 1 secondo (simulato dalla sospensione del thread). Il problema è che altri thread si bloccano e attendono il completamento dell'attività interrotta. Questo è un esempio forzato, ma immagina un'app servlet e qualcuno che invii un'attività di lunga durata al pool di join fork condiviso.

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
Cosa intendi con pool di thread personalizzato? Esiste un singolo ForkJoinPool comune, ma puoi sempre creare il tuo ForkJoinPool e inviargli richieste.
escluso il

7
Suggerimento: il campione Java Heinz Kabutz esamina lo stesso problema, ma con un impatto ancora peggiore: i thread deadlock del pool di join fork comune. Vedi javaspecialists.eu/archive/Issue223.html
Peti

Risposte:


395

Esiste effettivamente un trucco su come eseguire un'operazione parallela in un pool fork-join specifico. Se lo si esegue come attività in un pool fork-join, rimane lì e non utilizza quello comune.

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();
    }
}

Il trucco si basa su ForkJoinTask.fork che specifica: "Organizza l'esecuzione asincrona di questa attività nel pool in cui è in esecuzione l'attività corrente, se applicabile, oppure utilizzando ForkJoinPool.commonPool () se non inForkJoinPool ()"


20
I dettagli sulla soluzione sono descritti qui blog.krecan.net/2014/03/18/…
Lukas

3
Ma è anche specificato che i flussi usano il ForkJoinPooldettaglio dell'implementazione o è questo? Un collegamento alla documentazione sarebbe carino.
Nicolai,

6
@Lukas Grazie per lo snippet. Aggiungerò che l' ForkJoinPoolistanza dovrebbe essere shutdown()quando non è più necessaria per evitare una perdita di thread. (esempio)
jck,

5
Si noti che esiste un bug in Java 8 che, sebbene le attività siano in esecuzione su un'istanza del pool personalizzato, sono ancora accoppiate al pool condiviso: la dimensione del calcolo rimane proporzionale al pool comune e non al pool personalizzato. È stato corretto in Java 10: JDK-8190974
Terran

3
@terran Questo problema è stato risolto anche per Java 8 bugs.openjdk.java.net/browse/JDK-8224620
Cutberto Ocampo

192

I flussi paralleli utilizzano il valore predefinito ForkJoinPool.commonPoolche per impostazione predefinita ha un numero di thread in meno poiché si dispone di processori , come restituito da Runtime.getRuntime().availableProcessors()(Ciò significa che i flussi paralleli utilizzano tutti i processori perché utilizzano anche il thread principale):

Per applicazioni che richiedono pool separati o personalizzati, è possibile creare un ForkJoinPool con un determinato livello di parallelismo di destinazione; per impostazione predefinita, uguale al numero di processori disponibili.

Ciò significa anche che se sono stati avviati flussi paralleli nidificati o più flussi paralleli avviati contemporaneamente, condivideranno tutti lo stesso pool. Vantaggio: non userete mai più del valore predefinito (numero di processori disponibili). Svantaggio: potresti non assegnare "tutti i processori" assegnati a ciascun flusso parallelo che avvii (se ne hai più di uno). (Apparentemente puoi usare un ManagedBlocker per aggirare quello.)

Per modificare il modo in cui vengono eseguiti i flussi paralleli, è possibile

  • inviare l'esecuzione del flusso parallelo al proprio ForkJoinPool: yourFJP.submit(() -> stream.parallel().forEach(soSomething)).get();o
  • è possibile modificare le dimensioni del pool comune utilizzando le proprietà di sistema: System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20")per un parallelismo di destinazione di 20 thread. Tuttavia, questo non funziona più dopo la patch di backport https://bugs.openjdk.java.net/browse/JDK-8190974 .

Esempio di quest'ultimo sulla mia macchina che ha 8 processori. Se eseguo il seguente programma:

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) + " ");
});

L'output è:

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

Quindi puoi vedere che il flusso parallelo elabora 8 elementi alla volta, cioè usa 8 thread. Tuttavia, se rimuovo il commento alla riga commentata, l'output è:

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

Questa volta, il flusso parallelo ha utilizzato 20 thread e tutti i 20 elementi nel flusso sono stati elaborati contemporaneamente.


30
L' commonPoolha effettivamente uno in meno di availableProcessors, risultante in un parallelismo totale uguale a availableProcessorsperché il thread chiamante conta come uno.
Marko Topolnik,

2
invia il reso ForkJoinTask. Per imitare parallel() get()è necessario:stream.parallel().forEach(soSomething)).get();
Grigory Kislin,

5
Non sono convinto che ForkJoinPool.submit(() -> stream.forEach(...))eseguirà le mie azioni Stream con il dato ForkJoinPool. Mi aspetterei che l'intero Stream-Action venga eseguito nel ForJoinPool come UNA azione, ma internamente continui a utilizzare il ForkJoinPool predefinito / comune. Dove hai visto che ForkJoinPool.submit () avrebbe fatto quello che dici che fa?
Frederic Leitenberger,

@FredericLeitenberger Probabilmente intendevi mettere il tuo commento sotto la risposta di Lukas.
Assylias,

2
Vedo ora stackoverflow.com/a/34930831/1520422 mostra bene che funziona davvero come annunciato. Eppure ancora non capisco COME funzioni. Ma sto bene con "funziona". Grazie!
Frederic Leitenberger,

39

In alternativa al trucco di innescare il calcolo parallelo all'interno del tuo forkJoinPool puoi anche passare quel pool al metodo CompletableFuture.supplyAsync come in:

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

La soluzione originale (impostazione della proprietà di parallelismo comune ForkJoinPool) non funziona più. Guardando i collegamenti nella risposta originale, un aggiornamento che interrompe questo è stato riportato su Java 8. Come menzionato nei thread collegati, questa soluzione non ha funzionato per sempre. Sulla base di ciò, la soluzione è forkjoinpool.submit con la soluzione .get discussa nella risposta accettata. Penso che il backport risolva anche l'affidabilità di questa soluzione.

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();

Non vedo il cambiamento nel parallelismo quando faccio ForkJoinPool.commonPool().getParallelism()in modalità debug.
d-coder,

Grazie. Ho fatto alcuni test / ricerche e aggiornato la risposta. Sembra che un aggiornamento l'abbia modificato, poiché funziona nelle versioni precedenti.
Tod Casasent,

Perché continuo a ottenere questo: unreported exception InterruptedException; must be caught or declared to be thrownanche con tutte le catcheccezioni nel ciclo.
Rocky Li,

Rocky, non vedo alcun errore. Conoscere la versione di Java e la linea esatta aiuterà. "InterruptedException" suggerisce che la modalità try / catch in modalità sleep non è chiusa correttamente nella tua versione.
Tod Casasent,

13

Possiamo modificare il parallelismo predefinito usando la seguente proprietà:

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

quale può impostare per usare più parallelismo.


Sebbene sia un'impostazione globale, funziona per aumentare parallelStream
meadlai il

Questo ha funzionato per me sulla versione openjdk "1.8.0_222"
abbas

Stessa persona di cui sopra, questo non funziona per me su openjdk "11.0.6"
abbas

8

Per misurare il numero effettivo di thread utilizzati, è possibile verificare 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();

Questo può produrre su una CPU a 4 core un output come:

5 // common pool
23 // custom pool

Senza .parallel()esso dà:

3 // common pool
4 // custom pool

6
Thread.activeCount () non ti dice quali thread stanno elaborando il tuo stream. Mappare invece Thread.currentThread (). GetName (), seguito da un distinto (). Quindi ti renderai conto che non tutti i thread nel pool verranno utilizzati ... Aggiungi un ritardo alla tua elaborazione e verranno utilizzati tutti i thread nel pool.
keyoxy,

7

Fino ad ora ho usato le soluzioni descritte nelle risposte a questa domanda. Ora, ho creato una piccola libreria chiamata Parallel Stream Support per questo:

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

Ma come ha sottolineato @PabloMatiasGomez nei commenti, ci sono degli svantaggi riguardo al meccanismo di suddivisione dei flussi paralleli che dipende fortemente dalle dimensioni del pool comune. Vedere Flusso parallelo da un HashSet non eseguito in parallelo .

Sto usando questa soluzione solo per avere pool separati per diversi tipi di lavoro, ma non posso impostare la dimensione del pool comune su 1 anche se non lo uso.


4

Nota: sembra esserci una correzione implementata in JDK 10 che garantisce che il pool di thread personalizzati utilizzi il numero previsto di thread.

L'esecuzione di flussi paralleli all'interno di un ForkJoinPool personalizzato dovrebbe rispettare il parallelismo https://bugs.openjdk.java.net/browse/JDK-8190974


1

Ho provato il ForkJoinPool personalizzato come segue per regolare le dimensioni del pool:

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);
}

Ecco l'output che dice che il pool sta usando più thread rispetto al 4 predefinito .

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]

Ma in realtà c'è uno strano , quando ho cercato di ottenere lo stesso risultato usando ThreadPoolExecutorcome segue:

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

ma ho fallito.

Esso inizierà soltanto la parallelStream in un nuovo thread e quindi tutto il resto è lo stesso, che ancora una volta dimostra che la parallelStreamutilizzerà il ForkJoinPool di iniziare le sue thread figlio.


Quale potrebbe essere la possibile ragione dietro non consentire ad altri esecutori?
omjego,

@omjego Questa è una buona domanda, forse potresti iniziare una nuova domanda e fornire maggiori dettagli per elaborare le tue idee;)
Sentito il

1

Vai a prendere AbacusUtil . Il numero di thread può essere specificato per il flusso parallelo. Ecco il codice di esempio:

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

Divulgazione : Sono lo sviluppatore di AbacusUtil.


1

Se non vuoi fare affidamento su hack di implementazione, c'è sempre un modo per ottenere lo stesso implementando collezionisti personalizzati che combineranno mape collectsemantica ... e non ti limiteresti a ForkJoinPool:

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

Fortunatamente, è già fatto qui e disponibile su Maven Central: http://github.com/pivovarit/parallel-collectors

Disclaimer: l'ho scritto e me ne assumo la responsabilità.


0

Se non ti dispiace usare una libreria di terze parti, con cyclops-reazioni puoi mescolare stream sequenziali e paralleli all'interno della stessa pipeline e fornire ForkJoinPools personalizzati. Per esempio

 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()));

O se desideriamo continuare l'elaborazione all'interno di uno Stream sequenziale

 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);

[Divulgazione Sono lo sviluppatore principale di cyclops-reagire]


0

Se non hai bisogno di un ThreadPool personalizzato ma desideri limitare il numero di attività simultanee, puoi utilizzare:

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   
}));

(La domanda duplicata che richiede questo è bloccata, quindi per favore portami qui)


-2

puoi provare a implementare questo ForkJoinWorkerThreadFactory e iniettarlo nella classe 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();
    }

puoi usare questo costruttore del pool Fork-Join per fare questo.

note: - 1. se lo si utilizza, tenere presente che, in base all'implementazione di nuovi thread, la pianificazione da JVM sarà interessata, che generalmente pianifica thread fork-join su core diversi (trattati come thread computazionale). 2. la pianificazione delle attività tramite fork-join sui thread non verrà influenzata. 3. Non ho davvero capito come il flusso parallelo sta selezionando i thread dal fork-join (non è stato possibile trovare la documentazione corretta su di esso), quindi prova a utilizzare una diversa fabbrica di nomi thread in modo da assicurarti, se vengono selezionati i thread nel flusso parallelo da customThreadFactory che fornisci. 4. commonThreadPool non utilizzerà questo customThreadFactory.


Potete fornire un esempio utilizzabile che dimostrerebbe come utilizzare ciò che avete specificato?
J. Murray,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.