In che modo il framework fork / join è migliore di un pool di thread?


134

Quali sono i vantaggi dell'utilizzo del nuovo framework fork / join rispetto alla semplice suddivisione della grande attività in N sottoattività all'inizio, inviandole a un pool di thread nella cache (dagli Executors ) e aspettando che ogni attività venga completata? Non riesco a vedere come l'utilizzo dell'astrazione fork / join semplifichi il problema o renda la soluzione più efficiente rispetto a quella che abbiamo da anni.

Ad esempio, l'algoritmo di sfocatura parallelizzata nell'esempio dell'esercitazione potrebbe essere implementato in questo modo:

public class Blur implements Runnable {
    private int[] mSource;
    private int mStart;
    private int mLength;
    private int[] mDestination;

    private int mBlurWidth = 15; // Processing window size, should be odd.

    public ForkBlur(int[] src, int start, int length, int[] dst) {
        mSource = src;
        mStart = start;
        mLength = length;
        mDestination = dst;
    }

    public void run() {
        computeDirectly();
    }

    protected void computeDirectly() {
        // As in the example, omitted for brevity
    }
}

Dividi all'inizio e invia attività a un pool di thread:

// source image pixels are in src
// destination image pixels are in dst
// threadPool is a (cached) thread pool

int maxSize = 100000; // analogous to F-J's "sThreshold"
List<Future> futures = new ArrayList<Future>();

// Send stuff to thread pool:
for (int i = 0; i < src.length; i+= maxSize) {
    int size = Math.min(maxSize, src.length - i);
    ForkBlur task = new ForkBlur(src, i, size, dst);
    Future f = threadPool.submit(task);
    futures.add(f);
}

// Wait for all sent tasks to complete:
for (Future future : futures) {
    future.get();
}

// Done!

Le attività vanno alla coda del pool di thread, da cui vengono eseguite quando i thread di lavoro diventano disponibili. Finché la suddivisione è abbastanza granulare (per evitare di dover aspettare in particolare l'ultimo compito) e il pool di thread ha abbastanza thread (almeno N di processori), tutti i processori lavorano a piena velocità fino a quando non viene eseguito l'intero calcolo.

Mi sto perdendo qualcosa? Qual è il valore aggiunto dell'utilizzo del fork / join framework?

Risposte:


136

Penso che il malinteso di base sia che gli esempi Fork / Join NON mostrano come rubare il lavoro, ma solo una sorta di divisione e conquista standard.

Il furto di lavoro sarebbe così: il lavoratore B ha finito il suo lavoro. È gentile, quindi si guarda intorno e vede il Lavoratore A che lavora ancora duramente. Si avvicina e chiede: "Ehi ragazzo, potrei darti una mano." A risposte. "Bene, ho questo compito di 1000 unità. Finora ho finito 345 lasciando 655. Potresti per favore lavorare sul numero 673-1000, farò i 346-672." B dice "OK, cominciamo così possiamo andare al pub prima."

Vedete, i lavoratori devono comunicare tra loro anche quando hanno iniziato il vero lavoro. Questa è la parte mancante negli esempi.

Gli esempi invece mostrano solo qualcosa come "usa subappaltatori":

Operaio A: "Dang, ho 1000 unità di lavoro. Troppo per me. Farò 500 me stesso e subappalto 500 a qualcun altro." Questo continua fino a quando il grosso compito non viene suddiviso in piccoli pacchetti di 10 unità ciascuno. Questi saranno eseguiti dai lavoratori disponibili. Ma se un pacchetto è una specie di pillola di veleno e impiega molto più tempo di altri pacchetti - sfortuna, la fase di divisione è finita.

L'unica differenza rimanente tra Fork / Join e suddivisione dell'attività in anticipo è questa: quando si divide in anticipo la coda di lavoro è piena dall'inizio. Esempio: 1000 unità, la soglia è 10, quindi la coda ha 100 voci. Questi pacchetti sono distribuiti ai membri del threadpool.

Fork / Join è più complesso e cerca di ridurre il numero di pacchetti nella coda:

  • Passaggio 1: mettere in coda un pacchetto contenente (1 ... 1000)
  • Passaggio 2: un lavoratore apre il pacchetto (1 ... 1000) e lo sostituisce con due pacchetti: (1 ... 500) e (501 ... 1000).
  • Passaggio 3: un lavoratore apre il pacchetto (500 ... 1000) e spinge (500 ... 750) e (751 ... 1000).
  • Passaggio n: lo stack contiene questi pacchetti: (1..500), (500 ... 750), (750 ... 875) ... (991..1000)
  • Passaggio n + 1: il pacchetto (991..1000) viene visualizzato ed eseguito
  • Passaggio n + 2: il pacchetto (981..990) viene visualizzato ed eseguito
  • Passaggio n + 3: Il pacchetto (961..980) viene visualizzato e suddiviso in (961 ... 970) e (971..980). ....

Vedi: in Fork / Join la coda è più piccola (6 nell'esempio) e le fasi "split" e "work" sono interlacciate.

Quando più lavoratori saltano fuori e spingono contemporaneamente le interazioni non sono così chiare ovviamente.


Penso che questa sia davvero la risposta. Mi chiedo se ci siano esempi reali Fork / Join ovunque che dimostrerebbero anche il suo lavoro rubando le capacità? Con esempi elementari la quantità di carico di lavoro è abbastanza perfettamente prevedibile dalle dimensioni dell'unità (ad esempio la lunghezza dell'array), quindi la divisione iniziale è semplice. Il furto farebbe sicuramente differenza nei problemi in cui la quantità di carico di lavoro per unità non è ben prevedibile dalle dimensioni dell'unità.
Joonas Pulakka,

AH Se la tua risposta è corretta, non spiega come. L'esempio fornito da Oracle non comporta il furto di lavoro. Come funzionerebbe fork and join come nell'esempio che descrivi qui? Potresti mostrare del codice Java che farebbe fork e join rubare nel modo in cui lo descrivi? grazie
Marc

@Marc: mi dispiace, ma non ho esempi disponibili.
AH,

6
Il problema con l'esempio di Oracle, IMO, non è che non dimostra il furto di lavoro (lo fa, come descritto da AH) ma che è facile codificare un algoritmo per un semplice ThreadPool che fa altrettanto (come ha fatto Joonas). FJ è molto utile quando il lavoro non può essere suddiviso in compiti abbastanza indipendenti ma può essere ricorsivamente suddiviso in compiti indipendenti tra loro. Vedi la mia risposta per un esempio
Ashirley,

2
Alcuni esempi di casi in cui il furto di lavoro potrebbe tornare utile: h-online.com/developer/features/…
volley

27

Se hai n thread occupati che funzionano tutti al 100% in modo indipendente, sarà meglio di n thread in un pool Fork-Join (FJ). Ma non funziona mai così.

Potrebbe non essere possibile dividere esattamente il problema in n pezzi uguali. Anche se lo fai, la pianificazione dei thread è in qualche modo fuori dall'essere giusta. Finirai per aspettare il thread più lento. Se hai più attività, ognuna può essere eseguita con un parallelismo inferiore a n (generalmente più efficiente), ma può passare a n una volta quando altre attività sono terminate.

Quindi perché non tagliare semplicemente il problema in pezzi di dimensioni FJ e fare in modo che un pool di thread lavori su questo. L'utilizzo tipico di FJ taglia il problema in piccoli pezzi. Farlo in un ordine casuale richiede un grande coordinamento a livello hardware. Le spese generali sarebbero un assassino. In FJ, le attività vengono inserite in una coda che il thread legge nell'ordine Last In First Out (LIFO / stack) e il furto del lavoro (nel lavoro principale, in genere) viene eseguito First In First Out (FIFO / "coda"). Il risultato è che l'elaborazione di array lunghi può essere eseguita in gran parte in sequenza, anche se è suddivisa in piccoli blocchi. (È anche possibile che non sia banale spezzare il problema in piccoli pezzi di dimensioni uguali in un unico big bang. Diciamo di affrontare una qualche forma di gerarchia senza bilanciamento.)

Conclusione: FJ consente un uso più efficiente dei thread hardware in situazioni irregolari, che sarà sempre se si dispone di più thread.


Ma perché FJ non dovrebbe finire ad aspettare anche il thread più lento? Esiste un numero predeterminato di attività secondarie e, naturalmente, alcune di esse saranno sempre le ultime da completare. La regolazione del maxSizeparametro nel mio esempio produrrebbe una divisione di sottoattività quasi simile alla "suddivisione binaria" nell'esempio FJ (fatto all'interno del compute()metodo, che calcola qualcosa o invia sottoattività invokeAll()).
Joonas Pulakka,

Perché sono molto più piccoli - aggiungerò alla mia risposta.
Tom Hawtin - tackline

Ok, se il numero di sottoattività è di ordine (i) di grandezza maggiore di quello che può essere effettivamente elaborato in parallelo (il che ha senso, per evitare di dover aspettare l'ultimo), allora posso vedere i problemi di coordinamento. L'esempio di FJ potrebbe essere fuorviante se si suppone che la divisione sia quella granulare: utilizza una soglia di 100000, che per un'immagine di 1000x1000 produrrebbe 16 attività secondarie effettive, ciascuna delle quali elaborando 62500 elementi. Per un'immagine 10000x10000 ci sarebbero 1024 attività secondarie, che è già qualcosa.
Joonas Pulakka,

19

L'obiettivo finale dei pool di thread e Fork / Join sono simili: entrambi vogliono utilizzare la potenza della CPU disponibile il meglio che possono per la massima produttività. Il throughput massimo significa che il maggior numero possibile di attività deve essere completato in un lungo periodo di tempo. Cosa è necessario per farlo? (Per quanto segue supponiamo che non vi siano carenze di attività di calcolo: c'è sempre abbastanza da fare per un utilizzo della CPU al 100%. Inoltre uso "CPU" in modo equivalente per core o core virtuali in caso di hyper-threading).

  1. Almeno ci devono essere tanti thread in esecuzione quante sono le CPU disponibili, perché l'esecuzione di meno thread lascerà inutilizzato un core.
  2. Al massimo ci devono essere tanti thread in esecuzione quante sono le CPU disponibili, poiché l'esecuzione di più thread creerà un carico aggiuntivo per lo Scheduler che assegna CPU ai diversi thread, facendo sì che un certo tempo CPU passi allo scheduler piuttosto che alla nostra attività di calcolo.

Quindi abbiamo capito che per ottenere il massimo throughput dobbiamo avere lo stesso numero esatto di thread rispetto alle CPU. Nell'esempio di sfocatura di Oracle puoi sia prendere un pool di thread di dimensioni fisse con un numero di thread uguale al numero di CPU disponibili sia utilizzare un pool di thread. Non farà differenza, hai ragione!

Quindi, quando avrai problemi con un pool di thread? Cioè se un thread si blocca , perché il thread è in attesa che venga completata un'altra attività. Supponiamo il seguente esempio:

class AbcAlgorithm implements Runnable {
    public void run() {
        Future<StepAResult> aFuture = threadPool.submit(new ATask());
        StepBResult bResult = stepB();
        StepAResult aResult = aFuture.get();
        stepC(aResult, bResult);
    }
}

Quello che vediamo qui è un algoritmo che consiste in tre fasi A, B e C. A e B possono essere eseguite indipendentemente l'una dall'altra, ma la fase C richiede il risultato della fase A AND B. Ciò che questo algoritmo fa è inviare l'attività A a il threadpool ed eseguire l'attività b direttamente. Dopodiché il thread attenderà che anche l'attività A venga eseguita e continuerà con il passaggio C. Se A e B sono completati contemporaneamente, allora tutto va bene. E se A impiegasse più tempo di B? Ciò può essere dovuto al fatto che la natura dell'attività A lo impone, ma può anche essere il caso perché non è disponibile un thread per l'attività A all'inizio e l'attività A deve attendere. (Se è disponibile una sola CPU e quindi il tuo threadpool ha solo un singolo thread, ciò causerà persino un deadlock, ma per ora questo è oltre il punto). Il punto è che il thread che ha appena eseguito l'attività Bblocca l'intero thread . Dato che abbiamo lo stesso numero di thread delle CPU e un thread è bloccato, ciò significa che una CPU è inattiva .

Fork / Join risolve questo problema: nel framework fork / join dovresti scrivere lo stesso algoritmo come segue:

class AbcAlgorithm implements Runnable {
    public void run() {
        ATask aTask = new ATask());
        aTask.fork();
        StepBResult bResult = stepB();
        StepAResult aResult = aTask.join();
        stepC(aResult, bResult);
    }
}

Sembra lo stesso, no? Tuttavia, l'indizio è che aTask.join non si bloccherà . Invece qui è dove entra in gioco il furto di lavoro: il thread cercherà altri compiti che sono stati biforcati in passato e continuerà con quelli. Innanzitutto controlla se le attività che ha biforcato stesso hanno iniziato l'elaborazione. Quindi se A non è stato ancora avviato da un altro thread, farà A in un secondo momento, altrimenti controllerà la coda di altri thread e ruberà il loro lavoro. Una volta completata questa altra attività di un altro thread controllerà se A è stato completato ora. Se è l'algoritmo sopra può chiamare stepC. Altrimenti cercherà un altro compito da rubare. In questo modo i pool fork / join possono ottenere il 100% di utilizzo della CPU, anche a fronte di azioni di blocco .

Tuttavia c'è una trappola: rubare il lavoro è possibile solo per il joinrichiamo di ForkJoinTasks. Non può essere eseguito per azioni di blocco esterne come l'attesa di un altro thread o l'attesa di un'azione I / O. Quindi che dire, aspettare il completamento dell'I / O è un'attività comune? In questo caso, se potessimo aggiungere un thread aggiuntivo al pool Fork / Join che verrà nuovamente arrestato non appena l'azione di blocco sarà completata, sarà la seconda cosa migliore da fare. E in ForkJoinPoolrealtà possiamo fare proprio questo se stiamo usando ManagedBlockers.

Fibonacci

In JavaDoc per RecursiveTask è un esempio per il calcolo dei numeri di Fibonacci utilizzando Fork / Join. Per una classica soluzione ricorsiva vedere:

public static int fib(int n) {
    if (n <= 1) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}

Come spiegato in JavaDocs, questo è un modo piuttosto discarica di calcolare i numeri di fibonacci, poiché questo algoritmo ha una complessità O (2 ^ n) mentre sono possibili modi più semplici. Tuttavia questo algoritmo è molto semplice e facile da capire, quindi ci atteniamo ad esso. Supponiamo di voler accelerare questo con Fork / Join. Un'implementazione ingenua sarebbe simile a questa:

class Fibonacci extends RecursiveTask<Long> {
    private final long n;

    Fibonacci(long n) {
        this.n = n;
    }

    public Long compute() {
        if (n <= 1) {
            return n;
        }
        Fibonacci f1 = new Fibonacci(n - 1);
        f1.fork();
        Fibonacci f2 = new Fibonacci(n - 2);
        return f2.compute() + f1.join();
   }
}

I passaggi in cui è suddivisa questa attività sono troppo brevi e quindi questo si comporterà in modo orribile, ma puoi vedere come il framework generalmente funziona molto bene: i due riepiloghi possono essere calcolati in modo indipendente, ma quindi abbiamo bisogno di entrambi per costruire il finale risultato. Quindi la metà viene eseguita in un altro thread. Divertiti a fare lo stesso con i pool di thread senza ottenere un deadlock (possibile, ma non altrettanto semplice).

Solo per completezza: se desideri davvero calcolare i numeri di Fibonacci utilizzando questo approccio ricorsivo, ecco una versione ottimizzata:

class FibonacciBigSubtasks extends RecursiveTask<Long> {
    private final long n;

    FibonacciBigSubtasks(long n) {
        this.n = n;
    }

    public Long compute() {
        return fib(n);
    }

    private long fib(long n) {
        if (n <= 1) {
            return 1;
        }
        if (n > 10 && getSurplusQueuedTaskCount() < 2) {
            final FibonacciBigSubtasks f1 = new FibonacciBigSubtasks(n - 1);
            final FibonacciBigSubtasks f2 = new FibonacciBigSubtasks(n - 2);
            f1.fork();
            return f2.compute() + f1.join();
        } else {
            return fib(n - 1) + fib(n - 2);
        }
    }
}

Questo mantiene le attività secondarie molto più piccole perché vengono divise solo quando n > 10 && getSurplusQueuedTaskCount() < 2è vero, il che significa che ci sono significativamente più di 100 chiamate di metodo da fare ( n > 10) e non ci sono già molte attività man in attesa ( getSurplusQueuedTaskCount() < 2).

Sul mio computer (4 core (8 quando si conteggia Hyper-threading), CPU Intel (R) Core (TM) i7-2720QM a 2,20 GHz) ci fib(50)vogliono 64 secondi con l'approccio classico e solo 18 secondi con l'approccio Fork / Join che è un guadagno notevole, anche se teoricamente non è possibile.

Sommario

  • Sì, nel tuo esempio Fork / Join non ha alcun vantaggio rispetto ai pool di thread classici.
  • Fork / Join può migliorare drasticamente le prestazioni in caso di blocco
  • Fork / Join elude alcuni problemi di deadlock

17

Fork / join è diverso da un pool di thread perché implementa il furto del lavoro. Da Fork / Join

Come con qualsiasi ExecutorService, il framework fork / join distribuisce le attività ai thread di lavoro in un pool di thread. Il framework fork / join è distinto perché utilizza un algoritmo che ruba il lavoro. I thread di lavoro a corto di cose da fare possono rubare attività da altri thread ancora occupati.

Supponi di avere due thread e 4 attività a, b, c, d che richiedono rispettivamente 1, 1, 5 e 6 secondi. Inizialmente, aeb sono assegnati al thread 1 e c e d al thread 2. In un pool di thread, ciò richiederebbe 11 secondi. Con fork / join, il thread 1 termina e può rubare il lavoro dal thread 2, quindi l'attività d finirebbe per essere eseguita dal thread 1. Il thread 1 esegue a, b e d, il thread 2 solo c. Tempo complessivo: 8 secondi, non 11.

EDIT: Come sottolinea Joonas, le attività non sono necessariamente pre-assegnate a un thread. L'idea di fork / join è che un thread può scegliere di dividere un'attività in più sottoparti. Quindi, per ribadire quanto sopra:

Abbiamo due compiti (ab) e (cd) che richiedono rispettivamente 2 e 11 secondi. Il thread 1 inizia a eseguire ab e lo divide in due sottoattività a & b. Analogamente al thread 2, si divide in due sottoattività c & d. Quando il thread 1 ha terminato a & b, può rubare d dal thread 2.


5
I pool di thread sono in genere istanze ThreadPoolExecutor . In tal caso, le attività vanno in coda ( in pratica BlockingQueue ), da cui i thread di lavoro assumono le attività non appena hanno terminato l'attività precedente. Per quanto ne so, le attività non sono pre-assegnate a thread specifici. Ogni thread ha (al massimo) 1 attività alla volta.
Joonas Pulakka,

4
AFAIK esiste una coda per un ThreadPoolExecutor che a sua volta controlla diversi thread. Ciò significa che assegnando attività o Runnable (non Thread!) A un esecutore, le attività non sono preallocate su thread specifici. Esattamente come fa FJ. Finora nessun vantaggio per l'utilizzo di FJ.
AH,

1
@AH Sì, ma fork / join consente di dividere l'attività corrente. Il thread che sta eseguendo l'attività può dividerlo in due diverse attività. Quindi con ThreadPoolExecutor hai un elenco fisso di attività. Con fork / join, l'attività di esecuzione può dividere in due il proprio compito, che può quindi essere raccolto da altri thread al termine del loro lavoro. O tu se finisci per primo.
Matthew Farwell,

1
@Matthew Farwell: nell'esempio FJ , all'interno di ciascuna attività, compute()calcola l'attività o la divide in due attività secondarie. L'opzione scelta dipende solo dalle dimensioni if (mLength < sThreshold)...dell'attività ( ), quindi è solo un modo elegante per creare un numero fisso di attività. Per un'immagine 1000x1000, ci saranno esattamente 16 attività secondarie che calcolano effettivamente qualcosa. Inoltre, ci saranno 15 (= 16 - 1) attività "intermedie" che generano e invocano attività secondarie e non calcolano nulla da sole.
Joonas Pulakka,

2
@Matthew Farwell: È possibile che io non capisca tutto FJ, ma se una sottoattività ha deciso di eseguire il suo computeDirectly()metodo, non c'è più modo di rubare nulla. L'intera divisione viene eseguita a priori , almeno nell'esempio.
Joonas Pulakka,

14

Tutti sopra hanno ragione i vantaggi che si ottengono con il lavoro rubando, ma per espandere il perché.

Il vantaggio principale è l'efficiente coordinamento tra i thread di lavoro. Il lavoro deve essere suddiviso e riassemblato, il che richiede un coordinamento. Come puoi vedere nella risposta di AH sopra ogni thread ha la sua lista di lavoro. Una proprietà importante di questo elenco è che è ordinato (attività di grandi dimensioni nella parte superiore e attività di piccole dimensioni nella parte inferiore). Ogni thread esegue le attività in fondo al suo elenco e ruba le attività dalla cima degli altri elenchi di thread.

Il risultato di questo è:

  • Il capo e la coda degli elenchi di attività possono essere sincronizzati in modo indipendente, riducendo la contesa nell'elenco.
  • Sottotitoli significativi dell'opera vengono suddivisi e riassemblati dallo stesso thread, quindi non è necessario alcun coordinamento tra thread per questi sottotitoli.
  • Quando un thread ruba lavoro, ci vuole un pezzo grande che poi suddivide nel proprio elenco
  • L'acciaio di lavoro significa che i fili sono quasi completamente utilizzati fino alla fine del processo.

La maggior parte degli altri schemi di divisione e conquista che utilizzano pool di thread richiedono più comunicazione e coordinamento tra thread.


13

In questo esempio Fork / Join non aggiunge alcun valore perché non è necessario il fork e il carico di lavoro è suddiviso uniformemente tra i thread di lavoro. Fork / Join aggiunge solo spese generali.

Ecco un bell'articolo sull'argomento. Citazione:

Nel complesso, possiamo dire che ThreadPoolExecutor deve essere preferito laddove il carico di lavoro è uniformemente suddiviso tra thread di lavoro. Per essere in grado di garantirlo, è necessario conoscere esattamente l'aspetto dei dati di input. Al contrario, ForkJoinPool fornisce buone prestazioni indipendentemente dai dati di input ed è quindi una soluzione significativamente più robusta.


8

Un'altra differenza importante sembra essere che con FJ, puoi fare più "complesse" fasi. Considera il tipo di unione da http://faculty.ycp.edu/~dhovemey/spring2011/cs365/lecture/lecture18.html , ci sarebbe troppa orchestrazione richiesta per suddividere questo lavoro. ad es. devi fare le seguenti cose:

  • ordina il primo trimestre
  • ordinare il secondo trimestre
  • unisci i primi 2 trimestri
  • ordinare il terzo trimestre
  • ordina il quarto trimestre
  • unisci gli ultimi 2 trimestri
  • unire le 2 metà

Come si specifica che è necessario eseguire l'ordinamento prima delle fusioni che li riguardano, ecc.

Ho cercato il modo migliore per fare una determinata cosa per ciascuno di un elenco di elementi. Penso che pre-dividerò l'elenco e userò un ThreadPool standard. FJ sembra molto utile quando il lavoro non può essere suddiviso in compiti abbastanza indipendenti ma può essere suddiviso in modo ricorsivo in compiti che sono indipendenti tra loro (ad esempio, l'ordinamento delle metà è indipendente, ma non fondere le due metà ordinate in un tutto ordinato).


6

F / J ha anche un netto vantaggio quando si hanno costose operazioni di unione. Dal momento che si divide in una struttura ad albero, solo log2 (n) si fonde invece di n si fonde con la divisione lineare del thread. (Questo presuppone teoricamente che tu abbia tanti processori quanti thread, ma comunque un vantaggio) Per un compito a casa abbiamo dovuto unire diverse migliaia di array 2D (tutte le stesse dimensioni) sommando i valori di ciascun indice. Con i processori fork join e P il tempo si avvicina a log2 (n) mentre P si avvicina all'infinito.

1 2 3 .. 7 3 1 .... 8 5 4
4 5 6 + 2 4 3 => 6 9 9
7 8 9 .. 1 1 0 .... 8 9 9


3

Saresti stupito dalle prestazioni di ForkJoin in applicazioni come il cingolo. ecco il miglior tutorial da cui impareresti.

La logica di Fork / Join è molto semplice: (1) separare (fork) ogni attività di grandi dimensioni in attività più piccole; (2) elaborare ciascuna attività in un thread separato (separando quelle in attività ancora più piccole se necessario); (3) unisci i risultati.


3

Se il problema è tale che dobbiamo attendere il completamento di altri thread (come nel caso dell'ordinamento dell'array o della somma dell'array), utilizzare il fork fork, poiché Executor (Executors.newFixedThreadPool (2)) si strozzerà a causa del limitato numero di thread. Il pool forkjoin creerà più thread in questo caso per nascondere il thread bloccato per mantenere lo stesso parallelismo

Fonte: http://www.oracle.com/technetwork/articles/java/fork-join-422606.html

Il problema con gli esecutori per l'implementazione degli algoritmi di divisione e conquista non è correlato alla creazione di attività secondarie, poiché una Callable è libera di inviare una nuova attività secondaria al proprio esecutore e attendere il risultato in modo sincrono o asincrono. Il problema è quello del parallelismo: quando un callable attende il risultato di un altro callable, viene messo in uno stato di attesa, sprecando così l'opportunità di gestire un altro callable in coda per l'esecuzione.

Il framework fork / join aggiunto al pacchetto java.util.concurrent in Java SE 7 attraverso gli sforzi di Doug Lea colma questa lacuna

Fonte: https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ForkJoinPool.html

Il pool tenta di mantenere abbastanza thread attivi (o disponibili) aggiungendo, sospendendo o riprendendo in modo dinamico thread di lavoro interni, anche se alcune attività sono bloccate in attesa di unirsi ad altre. Tuttavia, tali regolazioni non sono garantite a fronte di IO bloccato o altra sincronizzazione non gestita

public int getPoolSize () Restituisce il numero di thread di lavoro che sono stati avviati ma non ancora terminati. Il risultato restituito da questo metodo può differire da getParallelism () quando vengono creati thread per mantenere il parallelismo quando altri vengono bloccati in modo cooperativo.


2

Vorrei aggiungere una risposta breve per coloro che non hanno molto tempo per leggere le risposte lunghe. Il confronto è tratto dal libro Applied Akka Patterns:

La tua decisione se utilizzare un fork-join-esecutore o un thread-pool-esecutore si basa in gran parte sul fatto che le operazioni in quel dispatcher saranno bloccate. Un fork-join -ecutor ti dà un numero massimo di thread attivi, mentre un thread-pool -ecutor ti dà un numero fisso di thread. Se i thread sono bloccati, un fork-join-execor ne creerà di più, mentre un thread-pool -ecutor no. Per le operazioni di blocco, in genere si sta meglio con un esecutore di pool di thread perché impedisce ai conteggi di thread di esplodere. Più operazioni "reattive" sono migliori in un fork-join-esecutore.

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.