Verschachtelte Java 8 parallele forEach-Schleife durchführen Armen. Ist dieses Verhalten zu erwarten?
Hinweis: ich habe bereits angesprochen, dieses problem in einem anderen, SO post - Die Verwendung einer semaphore innerhalb einer geschachtelten Java 8 parallel-stream handeln, kann der DEADLOCK. Ist das ein bug? -, aber der Titel von diesem post vorgeschlagen, dass das problem bezieht sich auf die Verwendung eines semaphor - die etwas abgelenkt der Diskussion. Ich bin zu erstellen, diese zu betonen, dass die nested loops-vielleicht ein performance-Problem, - obwohl beide Probleme haben wahrscheinlich eine gemeinsame Ursache (und vielleicht, weil es hat mich eine Menge Zeit, um herauszufinden, dieses problem). (Ich sehe es nicht als eine doppelte, denn Sie betonen ein weiteres symptom - aber wenn Sie das tun, löschen Sie ihn einfach).
Problem: Wenn Sie nest zwei Java 8 stream.parallel().forEach-Schleifen und alle Aufgaben sind unabhängig, staatenlos, etc. - außer für die übergabe an die gemeinsame FJ pool -, dann nisten parallele Schleife innerhalb eines parallelen Schleife führt, die viel ärmer als das verschachteln einer sequentiellen Schleife in eine parallele Schleife. Noch schlimmer: Wenn der Vorgang mit der inneren Schleife synchronisiert wird, erhalten Sie eine SACKGASSE.
Demonstration des performance-Problems
Ohne die "synchronisiert" Sie können immer noch beobachten, wie ein performance-problem. Finden Sie einen demo-code für diese an: http://svn.finmath.net/finmath%20experiments/trunk/src/net/finmath/experiments/concurrency/NestedParallelForEachTest.java
(siehe die JavaDoc gibt es eine ausführliche Beschreibung).
Unserem setup ist wie folgt: Wir haben eine geschachtelte stream.parallel().forEach().
- Die innere Schleife ist unabhängig (staatenlos, keine Störungen, etc. - außer der Verwendung einer gemeinsamen pool) und verbraucht 1 Sekunde in der Summe im schlimmsten Fall, nämlich dann, wenn sequentiell verarbeitet.
- Die Hälfte der Aufgaben der äußeren Schleife verbrauchen 10 Sekunden vor dieser Schleife.
- Hälfte verbrauchen 10 Sekunden nach dieser Schleife.
- Damit jeder thread verbraucht 11 Sekunden (worst case) insgesamt.
* Wir haben ein boolean, welcher erlaubt das Umschalten der inneren Schleife von der parallelen (), um sequentiell().
Nun: Vorlage 24 äußere-Schleife-Aufgaben zu einem pool mit Parallelität 8 wir würden erwarten, dass 24/8 * 11 = 33 Sekunden im besten Fall (auf einem 8-core oder besser Maschine).
Ist das Ergebnis:
- Mit inneren sequentiellen Schleife: 33 Sekunden.
- Mit innere parallele Schleife: >80 Sekunden (ich hatte 92 Sekunden).
Frage: Können Sie bestätigen, dass dieses Verhalten? Ist das etwas, was man erwarten würde, die aus dem Rahmen? (Ich bin ein bisschen vorsichtiger jetzt mit einer Forderung, dass das ein bug ist, aber ich persönlich glaube, dass es aufgrund von einen bug in der Implementierung von ForkJoinTask. Bemerkung: ich habe diese Gleichzeitigkeit-Interesse (siehe http://cs.oswego.edu/pipermail/concurrency-interest/2014-May/012652.html ), aber bisher habe ich keine Bestätigung bekommen von dort).
Demonstration der deadlock -
Dem folgenden code wird die DEADLOCK -
//Outer loop
IntStream.range(0,numberOfTasksInOuterLoop).parallel().forEach(i -> {
doWork();
synchronized(this) {
//Inner loop
IntStream.range(0,numberOfTasksInInnerLoop).parallel().forEach(j -> {
doWork();
});
}
});
wo numberOfTasksInOuterLoop = 24
, numberOfTasksInInnerLoop = 240
, outerLoopOverheadFactor = 10000
und doWork
einige Staatenlose CPU-burner.
Finden Sie eine komplette demo-code bei http://svn.finmath.net/finmath%20experiments/trunk/src/net/finmath/experiments/concurrency/NestedParallelForEachAndSynchronization.java
(siehe die JavaDoc gibt es eine ausführliche Beschreibung).
Ist dieses Verhalten zu erwarten? Beachten Sie, dass die Dokumentation der Java-parallel streams erwähnt keinerlei Problem mit der Verschachtelung oder Synchronisation. Auch die Tatsache, daß beide eine gemeinsame fork-join-pool ist nicht erwähnt.
Update
Weiteren test auf dem performance-Problem kann gefunden werden, an http://svn.finmath.net/finmath%20experiments/trunk/src/net/finmath/experiments/concurrency/NestedParallelForEachBenchmark.java
- dieser test kommen, ohne Blockierung der Bedienung (kein Gewinde.schlafen und nicht synchronisiert). Ich erstellte einige weitere Anmerkungen hier: http://christian-fries.de/blog/files/2014-nested-java-8-parallel-foreach.html
Update 2
Scheint es, als ob dieses problem und desto schwerer DEADLOCK mit Semaphoren wurde behoben in Java8 u40.
- Vielleicht lohnt sich verknüpfen die Gleichzeitigkeit Interesse Diskussion: cs.oswego.edu/pipermail/concurrency-interest/2014-May/...
- Dass die Parallelität Interesse die Diskussion ist sehr merkwürdig. Was mir auffällt ist, wie viel alle Mitglieder der Gruppe bestehen auf Dinge wie "der Programmierer sollte wissen, dass F/J ist xyz" oder "der Programmierer verwenden sollten <xyz von F/J framework>" für "diese" aber durch die Suche des gesamte Dokumentation
java.util.stream
fand ich keine Erwähnung der F/J framework überhaupt. Ich erinnere mich an die Erwähnung von F/J als detail irgendwo, aber das ist ein völlig anderes Bild. - Fries keine news auf, dass die Frage noch nicht?
Du musst angemeldet sein, um einen Kommentar abzugeben.
Das problem ist, dass die eher begrenzte Parallelität, den Sie konfiguriert haben, wird aufgefressen durch die äußere stream-Verarbeitung: wenn Sie sagen, dass Sie wollen, acht threads und verarbeiten eines Stroms von mehr als acht Elemente mit
parallel()
wird es schaffen, acht worker-threads und lassen Sie Sie verarbeiten Elemente.Dann in Ihren Verbraucher Sie sind der Verarbeitung ein anderer stream mit
parallel()
aber es gibt keine worker-threads zu linken. Da die worker-threads werden blockiert, wartet auf das Ende des der innere stream-Verarbeitung, dieForkJoinPool
hat, neues zu schaffen worker-threads, die gegen Ihre Parallelität konfiguriert. Es scheint mir, dass es nicht wieder diese erstrecken sich Fäden, aber Sie sterben zu lassen, rechts nach der Bearbeitung. Also in Ihrem inneren Verarbeitung, neue threads erstellt werden und entsorgt, das ist ein teurer Vorgang.Könnten Sie sehen es als Makel, dass die Einleitung von threads, nicht zur Berechnung einer parallelen stream-Verarbeitung aber nur auf das Ergebnis zu warten aber selbst wenn das behoben wurde haben Sie immer noch ein Allgemeines problem, das ist schwer (wenn überhaupt) zu beheben ist:
Wenn das Verhältnis zwischen der Anzahl der worker-threads, um äußere stream-Elemente ist gering, die Umsetzung verwenden Sie für den äußeren Strom, da Sie nicht wissen, dass der stream ist eine äußere stream. Also die Ausführung einer inneren Datenstrom in parallele Anfragen mehr worker-threads zur Verfügung. Mit den caller-thread für den Beitrag zur Berechnung beheben könnte es in einer Weise, dass die Leistung entspricht dem seriellen Berechnung aber immer einen Vorteil der parallelen Ausführung hier funktioniert nicht gut mit dem Konzept einer festen Anzahl von worker-threads.
Beachten Sie, dass Sie Kratzer auf der Oberfläche dieses problem hier, da haben Sie Recht ausgewogen Bearbeitungszeiten für die Elemente. Wenn die Verarbeitung der beiden, die inneren Elemente und die äußeren Elemente, abweichen (im Vergleich zu Elementen, die auf der gleichen Ebene), das problem wird sogar noch schlimmer.
Update: durch profiling und den code anzuschauen, es scheint, dass die
ForkJoinPool
hat versucht, die wartenden thread für "work stealing", sondern mit unterschiedlichen code abhängig von der Tatsache, ob dieThread
ist ein worker-thread oder einem anderen thread. Als ein Ergebnis, ein worker-thread ist eigentlich warten über 80% der Zeit, und tun sehr wenig, um nicht zu arbeiten, während andere threads wirklich dazu beitragen, um die Berechnung...Update 2: der Vollständigkeit halber, hier die einfache parallel-execution-Ansatz wie beschrieben in die Kommentare. Da es reiht jedes item es ist zu erwarten, dass zu viel overhead, wenn die Ausführungszeit für einen einzelnen Punkt ist eher klein. So ist es nicht eine anspruchsvolle Lösung, sondern eher eine demonstration, dass es möglich ist, mit langen ausführen-Aufgaben ohne viel Magie...
ForkJoinPool
nicht Versuch, einen Beitrag zur Berechnung, wenn der wartende thread ein worker-thread. Aber das hilft muss so ineffizient, dass zu warten, ohne zu helfen, ist viel schneller. Unglaublich. Oder, naja, ich lese so viel negatives über das ForkJoin Sache, dass es nicht verwundert mich jetzt.Thread
einen Unterschied macht. Das ist, warum ich sagte, dass die "work stealing" scheint besser zu funktionieren, wenn der initiierende thread ist ein nicht-worker-thread. Übrigens ich habe einige Tests mit einer alternative parallel-processing-implementation mit einem naiven "post jedes Element zu einemExecutorService
" - Ansatz. Und trotz der Umhüllung und queuing jedes Element overhead lief es in 32s bei Verwendung von 8 threads. Wenn also F/J führt, dass die Armen im Vergleich zu einem einfachen Ansatz, gehackt, in ein paar Stunden sollte es nicht sein, dieStream
backend.Stream
Problem, ich habe aktualisiert mein Antwort zu gehören der einfache Ansatz. Natürlich ist es nicht einen globalen Austausch derStream
parallele Implementierung, da solcher Eingriff ist nicht vorgesehen. Aber das aufrufen derstatic
MethodeparallelForEach(s, …)
ist fast so einfach wie das aufrufens.parallel().forEach(…)
...Kann ich bestätigen, das ist immer noch ein performance-Problem in 8u72, obwohl es nicht mehr deadlock. Parallel terminal-Operationen erfolgen weiterhin mit ForkJoinTask - Instanzen außerhalb eines ForkJoinPool Kontext, was bedeutet, dass jeden parallelen stream noch Aktien der gemeinsamen pool.
Zeigen eine einfache pathologischen Fall:
Den zweiten Lauf geht
innerLoop
zurunInNewPool
statt es direkt. Auf meiner Maschine (i7-4790, 8 CPU-threads), bekomme ich über einen 4x Geschwindigkeit-bis:Uncommenting die anderen print-Anweisungen, macht das problem offensichtlich:
Gemeinsamen pool worker-threads häufen sich in den synchronisierten block, wobei nur ein thread zu betreten zu einer Zeit. Da die innere parallel-Betrieb verwendet den gleichen pool, und alle anderen threads im pool die Sperre warten, bekommen wir die single-Thread-Ausführung.
Und das Ergebnis mit separaten ForkJoinPool Instanzen:
Wir haben immer noch die innere Schleife läuft auf einem worker-thread zu einer Zeit, aber die innere parallel-Betrieb bekommt eine frische Lache jedes mal und nutzen können, alle seine worker-threads.
Dies ist ein erfundenes Beispiel, aber das entfernen der synchronisierten Blöcke noch zeigt sich ein ähnlicher Unterschied in der Geschwindigkeit, da die inneren und äußeren Schleifen sind immer noch im Wettbewerb über die gleichen worker-threads. Multithread-Anwendungen müssen vorsichtig sein bei der Verwendung von parallelen streams in mehreren threads, wie könnte dies in willkürlicher Verlangsamung, wenn Sie sich überschneiden.
Dies ist ein Problem mit allen terminal-Operationen, nicht nur
forEach
, seit Sie laufen alle Aufgaben in der gemeinsamen pool. Ich bin mit derrunInNewPool
oben beschriebenen Methoden als ein workaround, aber das wird hoffentlich erstellt werden, in der standard-Bibliothek an einem gewissen Punkt.Nach dem Aufräumen der code ein wenig. Ich sehe nicht die gleichen Ergebnisse mit Java 8 update 45. Es ist zweifellos ein Aufwand, aber es ist sehr klein im Vergleich zu den Zeitspannen, die Sie sprechen.
Das Potenzial für einen deadlock zu rechnen, als Sie verbrauchen alle verfügbaren threads im pool mit der äußeren Schleife, so dass Sie keine threads Links zur Ausführung der inneren Schleife.
Folgende Programm druckt
Den code