Devo usare sempre un flusso parallelo quando possibile?


515

Con Java 8 e lambdas è facile scorrere le raccolte come flussi e altrettanto facile usare un flusso parallelo. Due esempi dai documenti , il secondo che utilizza parallelStream:

myShapesCollection.stream()
    .filter(e -> e.getColor() == Color.RED)
    .forEach(e -> System.out.println(e.getName()));

myShapesCollection.parallelStream() // <-- This one uses parallel
    .filter(e -> e.getColor() == Color.RED)
    .forEach(e -> System.out.println(e.getName()));

Fintanto che non mi interessa l'ordine, sarebbe sempre utile usare il parallelo? Si potrebbe pensare che sia più veloce dividere il lavoro su più core.

Ci sono altre considerazioni? Quando si dovrebbe usare il flusso parallelo e quando si dovrebbe usare il non parallelo?

(A questa domanda viene chiesto di avviare una discussione su come e quando utilizzare i flussi paralleli, non perché penso che utilizzarli sempre sia una buona idea.)

Risposte:


736

Un flusso parallelo ha un sovraccarico molto più elevato rispetto a uno sequenziale. Il coordinamento dei thread richiede molto tempo. Userei i flussi sequenziali di default e considererei solo quelli paralleli se

  • Ho una grande quantità di articoli da elaborare (o l'elaborazione di ogni articolo richiede tempo ed è parallelizzabile)

  • Ho un problema di prestazioni in primo luogo

  • Non eseguo già il processo in un ambiente multi-thread (ad esempio: in un contenitore Web, se ho già molte richieste da elaborare in parallelo, l'aggiunta di un ulteriore livello di parallelismo all'interno di ogni richiesta potrebbe avere effetti più negativi che positivi )

Nel tuo esempio, le prestazioni saranno comunque guidate dall'accesso sincronizzato System.out.println()e rendere questo processo parallelo non avrà alcun effetto, o addirittura negativo.

Inoltre, ricorda che i flussi paralleli non risolvono magicamente tutti i problemi di sincronizzazione. Se una risorsa condivisa viene utilizzata dai predicati e dalle funzioni utilizzate nel processo, è necessario assicurarsi che tutto sia sicuro per i thread. In particolare, gli effetti collaterali sono cose di cui ti devi veramente preoccupare se vai in parallelo.

In ogni caso, misura, non indovinare! Solo una misurazione ti dirà se ne vale la pena o meno il parallelismo.


18
Buona risposta. Aggiungo che se hai una grande quantità di articoli da elaborare, ciò aumenta solo i problemi di coordinamento del thread; è solo quando l'elaborazione di ciascun elemento richiede tempo ed è parallelizzabile che la parallelizzazione potrebbe essere utile.
Warren Dew,

16
@WarrenDew Non sono d'accordo. Il sistema Fork / Join dividerà semplicemente gli elementi N in, ad esempio, 4 parti ed elaborerà queste 4 parti in sequenza. I 4 risultati verranno quindi ridotti. Se il massiccio è davvero massiccio, anche per una rapida elaborazione dell'unità, la parallelizzazione può essere efficace. Ma come sempre, devi misurare.
JB Nizet,

ho una raccolta di oggetti che implementano Runnableche io chiamo start()per usarli come Threads, va bene cambiarlo con l'uso di java 8 stream in .forEach()parallelo? Quindi sarei in grado di rimuovere il codice thread dalla classe. Ma ci sono degli aspetti negativi?
ycomp

1
@JBNizet Se 4 parti vengono eseguite in sequenza, allora non vi è alcuna differenza se si tratta di processi paralleli o in sequenza? Pls chiarire
Harshana

3
@Harshana intende ovviamente che gli elementi di ciascuna delle 4 parti verranno elaborati in sequenza. Tuttavia, le parti stesse possono essere elaborate contemporaneamente. In altre parole, se sono disponibili più core della CPU, ciascuna parte può essere eseguita sul proprio core indipendentemente dalle altre parti, mentre elabora i propri elementi in sequenza. (NOTA: non so, se è così che funzionano i flussi Java paralleli, sto solo cercando di chiarire cosa intendesse JBNizet.)
Domani

258

L'API Stream è stata progettata per semplificare la scrittura di calcoli in un modo che è stato sottratto a come sarebbero stati eseguiti, rendendo semplice il passaggio tra sequenziale e parallelo.

Tuttavia, solo perché è facile, non significa che sia sempre una buona idea, e in effetti è una cattiva idea abbandonare.parallel() dappertutto semplicemente perché puoi.

Innanzitutto, si noti che il parallelismo non offre altri vantaggi oltre alla possibilità di un'esecuzione più rapida quando sono disponibili più core. Un'esecuzione parallela implicherà sempre più lavoro di una sequenziale, perché oltre a risolvere il problema, deve anche eseguire il dispacciamento e il coordinamento di compiti secondari. La speranza è che tu sia in grado di ottenere la risposta più velocemente suddividendo il lavoro su più processori; se ciò accada effettivamente dipende da molte cose, tra cui le dimensioni del set di dati, la quantità di calcolo che si sta eseguendo su ciascun elemento, la natura del calcolo (in particolare, l'elaborazione di un elemento interagisce con l'elaborazione di altri?) , il numero di processori disponibili e il numero di altre attività in competizione per tali processori.

Inoltre, si noti che il parallelismo spesso rivela anche il non determinismo nel calcolo che è spesso nascosto da implementazioni sequenziali; a volte questo non ha importanza o può essere mitigato limitando le operazioni coinvolte (ovvero, gli operatori di riduzione devono essere apolidi e associativi).

In realtà, a volte il parallelismo accelera il tuo calcolo, a volte no, a volte lo rallenta. È meglio sviluppare prima usando l'esecuzione sequenziale e quindi applicare il parallelismo dove

(A) sai che in realtà ci sono vantaggi nell'aumentare le prestazioni e

(B) che fornirà effettivamente prestazioni migliorate.

(A) è un problema aziendale, non tecnico. Se sei un esperto di prestazioni, di solito sarai in grado di guardare il codice e determinare (B), ma il percorso intelligente è misurare. (E non preoccuparti nemmeno finché non sei convinto di (A); se il codice è abbastanza veloce, meglio applicare i tuoi cicli cerebrali altrove.)

Il modello di prestazioni più semplice per il parallelismo è il modello "NQ", in cui N è il numero di elementi e Q è il calcolo per elemento. In generale, è necessario che il prodotto NQ superi una certa soglia prima di iniziare a ottenere un vantaggio in termini di prestazioni. Per un problema a bassa Q come "sommare i numeri da 1 a N", generalmente vedrai un pareggio tra N = 1000 e N = 10000. Con problemi di Q più alti, vedrai breakevens a soglie più basse.

Ma la realtà è piuttosto complicata. Quindi fino a quando non raggiungi l'esperienza, prima identifica quando l'elaborazione sequenziale ti sta effettivamente costando qualcosa, quindi misura se il parallelismo ti aiuterà.


18
Questo post fornisce ulteriori dettagli sul modello NQ: gee.cs.oswego.edu/dl/html/StreamParallelGuidance.html
Pino

4
@specializt: commutazione da un flusso sequenziale parallelo fa cambiare l'algoritmo (nella maggior parte dei casi). Il determinismo menzionato qui riguarda le proprietà su cui gli operatori (arbitrari) potrebbero fare affidamento (l'implementazione dello Stream non può saperlo), ma ovviamente non dovrebbe fare affidamento. Ecco cosa ha cercato di dire quella sezione di questa risposta. Se ti interessano le regole, puoi avere un risultato deterministico, proprio come dici, (altrimenti i flussi paralleli erano abbastanza inutili), ma c'è anche la possibilità di non determinismo intenzionalmente consentito, come quando si utilizza findAnyinvece di findFirst...
Holger

4
"In primo luogo, nota che il parallelismo non offre altri benefici oltre alla possibilità di un'esecuzione più rapida quando sono disponibili più core" - o se stai applicando un'azione che coinvolge IO (ad es myListOfURLs.stream().map((url) -> downloadPage(url))....).
Jules il

6
@Pacerier Questa è una bella teoria, ma purtroppo ingenua (per iniziare, vedi la storia trentennale dei tentativi di costruire compilatori con parallelismo automatico). Dal momento che non è pratico indovinare il momento giusto per non infastidire l'utente quando inevitabilmente sbagliamo, la cosa responsabile da fare era solo lasciare che l'utente dicesse quello che voleva. Per la maggior parte delle situazioni, l'impostazione predefinita (sequenziale) è corretta e più prevedibile.
Brian Goetz,

2
@Jules: non usare mai flussi paralleli per IO. Sono pensati esclusivamente per operazioni ad alta intensità di CPU. I flussi paralleli vengono utilizzati ForkJoinPool.commonPool()e non si desidera che le attività di blocco vengano eseguite lì.
R2C2

68

Ho visto una delle presentazioni di Brian Goetz (Java Language Architect e responsabile delle specifiche per Lambda Expressions) . Spiega in dettaglio i seguenti 4 punti da considerare prima di procedere alla parallelizzazione:

Costi di divisione / decomposizione
- A volte la divisione è più costosa della semplice esecuzione del lavoro!
Costi di invio / gestione attività
- Può svolgere molto lavoro nel tempo necessario per passare a un altro thread.
Costi della combinazione di risultati
: a volte la combinazione comporta la copia di molti dati. Ad esempio, aggiungere numeri è economico mentre l'unione di set è costosa.
Località
- L'elefante nella stanza. Questo è un punto importante che tutti potrebbero perdere. Dovresti considerare i mancati cache, se una CPU attende i dati a causa dei mancati cache, non otterresti nulla dalla parallelizzazione. Questo è il motivo per cui le fonti basate su array parallelizzano il meglio poiché gli indici successivi (vicino all'indice corrente) vengono memorizzati nella cache e ci sono meno possibilità che la CPU subisca un errore nella cache.

Cita anche una formula relativamente semplice per determinare una possibilità di accelerazione parallela.

Modello NQ :

N x Q > 10000

dove,
N = numero di elementi di dati
Q = quantità di lavoro per articolo


13

JB ha colpito l'unghia sulla testa. L'unica cosa che posso aggiungere è che Java 8 non esegue una pura elaborazione parallela, ma in modo paraquenziale . Sì, ho scritto l'articolo e faccio F / J da trent'anni, quindi capisco il problema.


10
Gli stream non sono iterabili perché gli stream eseguono iterazione interna anziché esterna. Questa è comunque la ragione di tutti i flussi. Se hai problemi con il lavoro accademico, la programmazione funzionale potrebbe non fare per te. Programmazione funzionale === matematica === accademica. E no, J8-FJ non è rotto, è solo che la maggior parte delle persone non legge il manuale di f ******. I documenti java dicono molto chiaramente che non è un framework di esecuzione parallelo. Questa è l'intera ragione di tutto ciò che riguarda lo splitterator. Sì, è accademico, sì, funziona se sai come usarlo. Sì, dovrebbe essere più semplice usare un esecutore personalizzato
Kr0e,

1
Stream ha un metodo iterator (), quindi puoi iterarli all'esterno se vuoi. La mia comprensione era che non implementano Iterable perché puoi usare quell'iteratore solo una volta e nessuno poteva decidere se andava bene.
Trejkaz,

14
a dire il vero: il tuo intero articolo sembra un rant massiccio ed elaborato - e questo praticamente nega la sua credibilità ... consiglierei di rifarlo con un sottotono molto meno aggressivo, altrimenti non molte persone si preoccuperebbero di leggerlo completamente ... sono solo Sayan
specializt

Un paio di domande sul tuo articolo ... prima di tutto, perché apparentemente equipari le strutture ad albero bilanciate con i grafici aciclici diretti? Sì, gli alberi bilanciati sono DAG, ma lo sono anche gli elenchi collegati e praticamente ogni struttura di dati orientata agli oggetti diversa dalle matrici. Inoltre, quando dici che la decomposizione ricorsiva funziona solo su strutture ad albero bilanciate e non è quindi rilevante dal punto di vista commerciale, come giustifichi questa affermazione? Mi sembra (è vero senza approfondire davvero il problema) che dovrebbe funzionare altrettanto bene su strutture di dati basate su array, ad esempio ArrayList/ HashMap.
Jules il

1
Questa discussione è del 2013, da allora molte cose sono cambiate. Questa sezione è per commenti non risposte dettagliate.
escluso il

3

Altre risposte hanno già trattato la profilazione per evitare l'ottimizzazione prematura e i costi generali nell'elaborazione parallela. Questa risposta spiega la scelta ideale delle strutture di dati per lo streaming parallelo.

Come regola generale, miglioramento delle prestazioni di parallelismo sono migliori sui flussi sopra ArrayList, HashMap, HashSet, e ConcurrentHashMaple istanze; array; intintervalli; e longintervalli. Ciò che queste strutture di dati hanno in comune è che possono essere tutte suddivise in modo accurato ed economico in subrange di qualsiasi dimensione desiderata, il che rende facile dividere il lavoro tra thread paralleli. L'astrazione utilizzata dalla libreria di flussi per eseguire questa attività è lo spliterator, che viene restituito dal spliteratormetodo su Streame Iterable.

Un altro fattore importante che tutte queste strutture di dati hanno in comune è che forniscono una località di riferimento da buona a eccellente quando vengono elaborate in sequenza: i riferimenti ad elementi sequenziali sono memorizzati insieme. Gli oggetti a cui fanno riferimento tali riferimenti potrebbero non essere vicini l'uno all'altro in memoria, il che riduce la località di riferimento. La località di riferimento risulta essere di fondamentale importanza per la parallelizzazione di operazioni in blocco: senza di essa, i thread trascorrono gran parte del loro tempo inattivi, in attesa che i dati vengano trasferiti dalla memoria nella cache del processore. Le strutture di dati con la migliore località di riferimento sono matrici primitive perché i dati stessi vengono archiviati contigui nella memoria.

Fonte: oggetto n. 48 Usare cautela quando si eseguono flussi paralleli, effettivi Java 3e di Joshua Bloch


2

Non parallelizzare mai un flusso infinito con un limite. Ecco cosa succede:

    public static void main(String[] args) {
        // let's count to 1 in parallel
        System.out.println(
            IntStream.iterate(0, i -> i + 1)
                .parallel()
                .skip(1)
                .findFirst()
                .getAsInt());
    }

Risultato

    Exception in thread "main" java.lang.OutOfMemoryError
        at ...
        at java.base/java.util.stream.IntPipeline.findFirst(IntPipeline.java:528)
        at InfiniteTest.main(InfiniteTest.java:24)
    Caused by: java.lang.OutOfMemoryError: Java heap space
        at java.base/java.util.stream.SpinedBuffer$OfInt.newArray(SpinedBuffer.java:750)
        at ...

Lo stesso se usi .limit(...)

Spiegazione qui: Java 8, l'utilizzo di .parallel in un flusso provoca errori OOM

Allo stesso modo, non usare il parallelo se lo stream è ordinato e contiene molti più elementi di quelli che si desidera elaborare, ad es

public static void main(String[] args) {
    // let's count to 1 in parallel
    System.out.println(
            IntStream.range(1, 1000_000_000)
                    .parallel()
                    .skip(100)
                    .findFirst()
                    .getAsInt());
}

Questo potrebbe durare molto più a lungo perché i thread paralleli potrebbero funzionare su molti intervalli di numeri anziché su quello cruciale 0-100, causando un tempo molto lungo.

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.