it-swarm.com.de

Benutzerdefinierter Thread-Pool in Java 8-Parallelstream

Kann ein benutzerdefinierter Thread-Pool für Java 8 Parallel Stream angegeben werden? Ich kann es nirgendwo finden. 

Stellen Sie sich vor, ich habe eine Serveranwendung und möchte parallele Streams verwenden. Aber die Anwendung ist groß und multithreaded, deshalb möchte ich sie unterteilen. Ich möchte nicht, dass in einem Modul der Applicationblock-Tasks ein langsam ausgeführter Task von einem anderen Modul ausgeführt wird.

Wenn ich unterschiedliche Thread-Pools nicht für verschiedene Module verwenden kann, bedeutet dies, dass ich in den meisten Situationen der Realität keine sicheren parallelen Streams verwenden kann.

Versuchen Sie das folgende Beispiel. Einige CPU-intensive Tasks werden in separaten Threads ausgeführt. Die Tasks nutzen parallele Streams. Die erste Aufgabe ist fehlgeschlagen, sodass jeder Schritt 1 Sekunde dauert (simuliert durch Thread-Sleep). Das Problem ist, dass andere Threads hängen bleiben und warten, bis die fehlerhafte Aufgabe abgeschlossen ist. Dies ist nur ein Beispiel, aber stellen Sie sich eine Servlet-App und eine Person vor, die eine lange laufende Aufgabe an den gemeinsam genutzten Fork-Join-Pool sendet. 

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

Es gibt tatsächlich einen Trick, wie eine Paralleloperation in einem bestimmten Fork-Join-Pool ausgeführt werden kann. Wenn Sie es als Task in einem Fork-Join-Pool ausführen, bleibt es dort und verwendet nicht den üblichen. 

ForkJoinPool forkJoinPool = new ForkJoinPool(2);
forkJoinPool.submit(() ->
    //parallel task here, for example
    IntStream.range(1, 1_000_000).parallel().filter(PrimesPrint::isPrime).collect(toList())
).get();

Der Trick basiert auf ForkJoinTask.fork , das Folgendes angibt: "Diese Task wird asynchron im Pool ausgeführt, in dem die aktuelle Task ausgeführt wird (falls zutreffend), oder ForkJoinPool.commonPool (), falls nicht inForkJoinPool ()."

323
Lukas

Die parallelen Streams verwenden den Standardcode ForkJoinPool.commonPool, der standardmäßig hat einen weniger Threads als Sie über Prozessoren verfügen , wie von Runtime.getRuntime().availableProcessors() zurückgegeben.

Für Anwendungen, die separate oder benutzerdefinierte Pools erfordern, kann ein ForkJoinPool mit einem bestimmten Ziel-Parallelitätsgrad erstellt werden. entspricht standardmäßig der Anzahl der verfügbaren Prozessoren.

Das bedeutet auch, wenn Sie parallele Streams geschachtelt oder mehrere parallele Streams gleichzeitig gestartet haben, werden alle share denselben Pool. Vorteil: Sie verwenden niemals mehr als die Standardeinstellung (Anzahl verfügbarer Prozessoren). Nachteil: Möglicherweise werden nicht jedem Prozessor, den Sie initiieren, "alle Prozessoren" zugewiesen (wenn Sie mehr als einen haben). (Anscheinend können Sie einen ManagedBlocker verwenden, um das zu umgehen.)

Um die Art und Weise zu ändern, in der parallele Streams ausgeführt werden, können Sie dies tun

  • senden Sie die parallele Stream-Ausführung an Ihren eigenen ForkJoinPool: yourFJP.submit(() -> stream.parallel().forEach(soSomething)).get(); oder
  • sie können die Größe des allgemeinen Pools mithilfe der Systemeigenschaften ändern: System.setProperty("Java.util.concurrent.ForkJoinPool.common.parallelism", "20") für eine Zielparallelität von 20 Threads.

Beispiel für Letzteres auf meiner Maschine, die über 8 Prozessoren verfügt. Wenn ich das folgende Programm ausführen:

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

Die Ausgabe ist:

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

Sie können also sehen, dass der parallele Stream 8 Elemente gleichzeitig verarbeitet, d. H. 8 Threads verwendet. Wenn ich die kommentierte Zeile jedoch nicht kommentiere, wird folgende Ausgabe ausgegeben:

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

Diesmal hat der parallele Stream 20 Threads verwendet, und alle 20 Elemente im Stream wurden gleichzeitig verarbeitet.

166
assylias

Alternativ zum Trick, die parallele Berechnung in Ihrem eigenen forkJoinPool auszulösen, können Sie diesen Pool auch an die CompletableFuture.supplyAsync-Methode übergeben, wie 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
);
32
Mario Fusco

Wenn Sie einen ForkJoinPool verwenden und einen parallelen Stream einreichen, werden nicht alle Threads zuverlässig verwendet. Wenn Sie sich dies anschauen ( Parallel Stream aus einem HashSet läuft nicht parallel ) und dies ( Warum verwendet der Parallel Stream nicht alle Threads des ForkJoinPool? ), werden Sie die Argumentation.

Kurzversion: Wenn ForkJoinPool/submit für Sie nicht funktioniert, verwenden Sie 

System.setProperty("Java.util.concurrent.ForkJoinPool.common.parallelism", "10");
16
Tod Casasent

Bisher habe ich die in den Antworten dieser Frage beschriebenen Lösungen verwendet. Nun kam ich mit einer kleinen Bibliothek namens Parallel Stream Support dazu:

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

Wie @PabloMatiasGomez in den Kommentaren feststellt, gibt es jedoch Nachteile hinsichtlich des Aufteilungsmechanismus paralleler Streams, der stark von der Größe des gemeinsamen Pools abhängt. Siehe Paralleler Stream von einem HashSet läuft nicht parallel .

Ich verwende diese Lösung nur, um separate Pools für verschiedene Arten von Arbeit zu haben, aber ich kann die Größe des gemeinsamen Pools nicht auf 1 festlegen, selbst wenn ich ihn nicht verwende.

7
Stefan Ferstl

Um die tatsächliche Anzahl der verwendeten Threads zu messen, können Sie Thread.activeCount() überprüfen:

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

Dies kann auf einer 4-Kern-CPU eine Ausgabe wie die folgende erzeugen:

5 // common pool
23 // custom pool

Ohne .parallel() gibt es:

3 // common pool
4 // custom pool
7
charlie

Note: In JDK 10 scheint ein Fix implementiert worden zu sein, das sicherstellt, dass der Custom Thread Pool die erwartete Anzahl von Threads verwendet.

Die parallele Stream-Ausführung in einem benutzerdefinierten ForkJoinPool sollte der Parallelität folgen https://bugs.openjdk.Java.net/browse/JDK-8190974

3
Scott Langley

Gehen Sie zu AbacusUtil . Die Thread-Nummer kann für den parallelen Stream angegeben werden. Hier ist der Beispielcode:

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

Offenlegung : Ich bin der Entwickler von AbacusUtil.

1
user_3380739

Wenn Sie sich nicht auf Implementierungs-Hacks verlassen möchten, gibt es immer einen Weg, das gleiche zu erreichen, indem Sie benutzerdefinierte Kollektoren implementieren, die map und collect-Semantik kombinieren ... und Sie wären nicht auf ForkJoinPool beschränkt:

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

Zum Glück ist es bereits hier und auf Maven Central verfügbar: http://github.com/pivovarit/parallel-collectors

Haftungsausschluss: Ich habe es geschrieben und übernehme die Verantwortung dafür.

0

Wir können die Standardparallelität mit der folgenden Eigenschaft ändern:

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

die eingerichtet werden kann, um mehr Parallelität zu verwenden.

0
KayV

Wenn Sie keine Fremdanbieter-Bibliothek verwenden möchten, können Sie mit cyclops-react sequentielle und parallele Streams innerhalb derselben Pipeline mischen und benutzerdefinierte ForkJoinPools bereitstellen. Zum Beispiel

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

Oder wenn wir die Verarbeitung innerhalb eines sequentiellen Streams fortsetzen möchten

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

[Offenlegung Ich bin der Hauptentwickler von Cyclops-React]

0
John McClean

Wenn Sie keinen benutzerdefinierten ThreadPool benötigen, sondern die Anzahl der gleichzeitigen Aufgaben begrenzen möchten, können Sie Folgendes verwenden:

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

(Eine doppelte Frage, die danach fragt, ist gesperrt, also tragen Sie mich bitte hier.)

0
Martin Vseticka

Ich habe den custom ForkJoinPool wie folgt versucht, um die Poolgröße anzupassen:

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

Hier ist die Ausgabe, die besagt, dass der Pool mehr Threads verwendet als die Standardeinstellung 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]

Aber tatsächlich gibt es ein Weirdo , als ich versuchte, dasselbe Ergebnis mit ThreadPoolExecutor wie folgt zu erreichen:

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

aber ich habe versagt 

Es wird nur den parallelStream in einem neuen Thread starten und dann ist alles andere gleich, was noch einmal beweist, dass die parallelStream den ForkJoinPool verwendet, um seine untergeordneten Threads zu starten. 

0
Hearen