ฉันควรใช้กระแสข้อมูลแบบขนานเสมอเมื่อทำได้


514

ด้วย Java 8 และ lambdas ทำให้ง่ายต่อการวนซ้ำคอลเลกชันเป็นสตรีมและใช้สตรีมแบบขนานได้อย่างง่ายดาย ตัวอย่างสองตัวอย่างจากเอกสารตัวอย่างที่สองใช้ 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()));

ตราบใดที่ฉันไม่สนใจคำสั่งมันจะเป็นประโยชน์กับการใช้ขนานหรือไม่? ใครจะคิดว่ามันเร็วกว่าการแบ่งงานบนแกนเพิ่มเติม

มีข้อควรพิจารณาอื่น ๆ อีกไหม? ควรใช้สตรีมแบบขนานเมื่อใดและควรใช้สตรีมแบบไม่ขนานเมื่อใด

(คำถามนี้ขอให้กระตุ้นการสนทนาเกี่ยวกับวิธีการและเวลาในการใช้สตรีมแบบขนานไม่ใช่เพราะฉันคิดว่าการใช้พวกเขาเป็นความคิดที่ดีเสมอ)

คำตอบ:


735

กระแสขนานมีค่าใช้จ่ายที่สูงกว่ามากเมื่อเทียบกับลำดับ การประสานงานเธรดใช้เวลานานพอสมควร ฉันจะใช้ลำธารเรียงตามค่าเริ่มต้นและพิจารณาเพียงลำพังขนานถ้า

  • ฉันมีรายการจำนวนมากที่ต้องดำเนินการ (หรือการประมวลผลของแต่ละรายการต้องใช้เวลาและสามารถขนานกันได้)

  • ฉันมีปัญหาเรื่องประสิทธิภาพในตอนแรก

  • ฉันไม่ได้เรียกใช้กระบวนการในสภาพแวดล้อมแบบมัลติเธรด (ตัวอย่างเช่น: ในเว็บคอนเทนเนอร์ถ้าฉันมีคำร้องขอให้ดำเนินการแบบขนานจำนวนมากแล้วการเพิ่มเลเยอร์คู่ขนานในแต่ละคำร้องขออาจมีผลลบมากกว่าผลบวก )

ในตัวอย่างของคุณประสิทธิภาพจะถูกผลักดันโดยการเข้าถึงที่มีการซิงโครไนซ์System.out.println()และการทำให้กระบวนการนี้ขนานกันจะไม่มีผลกระทบใด ๆ หรือแม้แต่การลบ

นอกจากนี้โปรดทราบว่าสตรีมขนานไม่ได้แก้ปัญหาการซิงโครไนซ์ทั้งหมดอย่างน่าอัศจรรย์ หากมีการใช้ทรัพยากรที่ใช้ร่วมกันโดยเพรดิเคตและฟังก์ชันที่ใช้ในกระบวนการคุณจะต้องตรวจสอบให้แน่ใจว่าทุกอย่างปลอดภัยต่อเธรด โดยเฉพาะอย่างยิ่งผลข้างเคียงเป็นสิ่งที่คุณต้องกังวลถ้าคุณไปขนานกัน

ไม่ว่าในกรณีใด ๆ วัดอย่าเดา! เฉพาะการวัดเท่านั้นที่จะบอกคุณได้ว่าการขนานนั้นมีค่าหรือไม่


18
คำตอบที่ดี. ฉันจะเพิ่มว่าถ้าคุณมีรายการจำนวนมากในการประมวลผลที่เพิ่มปัญหาการประสานงานของเธรดเท่านั้น มันก็ต่อเมื่อการประมวลผลของแต่ละรายการต้องใช้เวลาและสามารถขนานกันได้ว่าการขนานอาจมีประโยชน์
Warren Dew

16
@WarrenDew ฉันไม่เห็นด้วย ระบบ Fork / Join จะแยกรายการ N เป็นตัวอย่างเช่น 4 ส่วนและประมวลผล 4 ส่วนตามลำดับ ผลลัพธ์ทั้ง 4 จะลดลง หากมีขนาดใหญ่มากถึงแม้จะใช้หน่วยประมวลผลที่รวดเร็วการขนานก็มีประสิทธิภาพ แต่เช่นเคยคุณต้องวัด
JB Nizet

ฉันมีชุดของวัตถุที่ใช้Runnableที่ฉันเรียกstart()ใช้พวกเขาเป็นThreadsมันตกลงเปลี่ยนเป็นใช้ java 8 กระแสใน.forEach()ขนาน? จากนั้นฉันจะสามารถดึงเธรดโค้ดออกจากคลาสได้ แต่มีข้อเสียอะไรบ้าง?
ycomp

1
@JBNizet หาก 4 ส่วน pocess ตามลำดับดังนั้นจะไม่มีความแตกต่างในการประมวลผลแนวหรือรู้ตามลำดับ? กรุณาชี้แจง
Harshana

3
@Harshana เขาเห็นได้ชัดว่าหมายความว่าองค์ประกอบของแต่ละส่วน 4 ส่วนจะถูกประมวลผลตามลำดับ อย่างไรก็ตามชิ้นส่วนเองอาจถูกประมวลผลพร้อมกัน กล่าวอีกนัยหนึ่งถ้าคุณมีคอร์ CPU หลายตัวแต่ละส่วนสามารถทำงานบนแกนของตัวเองโดยไม่ขึ้นกับส่วนอื่น ๆ ในขณะที่ประมวลผลองค์ประกอบของตัวเองตามลำดับ (หมายเหตุ: ผมไม่ทราบว่าถ้านี้เป็นวิธีที่ขนาน Java ลำธารทำงานฉันแค่พยายามที่จะชี้แจงสิ่งที่หมาย JBNizet.)
วันพรุ่งนี้

258

Stream API ได้รับการออกแบบมาเพื่อให้ง่ายต่อการเขียนการคำนวณด้วยวิธีที่แยกออกจากวิธีที่พวกเขาจะถูกดำเนินการทำให้การสลับระหว่างลำดับและขนานง่าย

อย่างไรก็ตามเนื่องจากง่ายไม่ได้หมายความว่ามันเป็นความคิดที่ดีเสมอและในความเป็นจริงมันเป็นความคิดที่ดีที่จะทิ้ง.parallel()ทุกที่ไว้เพียงเพราะคุณทำได้

ก่อนอื่นให้สังเกตว่าการขนานกันนั้นไม่ได้มีประโยชน์อะไรนอกจากความเป็นไปได้ของการดำเนินการที่รวดเร็วกว่าเมื่อมีแกนประมวลผลเพิ่มเติม การดำเนินการแบบขนานจะเกี่ยวข้องกับการทำงานมากกว่าลำดับต่อเนื่องเสมอเนื่องจากนอกเหนือจากการแก้ปัญหาแล้วยังต้องทำการจัดส่งและประสานงานของงานย่อยอีกด้วย ความหวังคือคุณจะได้รับคำตอบเร็วขึ้นโดยการแบ่งงานระหว่างโปรเซสเซอร์หลายตัว ไม่ว่าจะเกิดขึ้นจริงขึ้นอยู่กับหลายสิ่งรวมถึงขนาดของชุดข้อมูลของคุณการคำนวณที่คุณทำกับแต่ละองค์ประกอบธรรมชาติของการคำนวณ (โดยเฉพาะการประมวลผลขององค์ประกอบหนึ่งโต้ตอบกับการประมวลผลของผู้อื่นหรือไม่) จำนวนตัวประมวลผลที่มีอยู่และจำนวนของงานอื่นที่แข่งขันกันสำหรับตัวประมวลผลเหล่านั้น

นอกจากนี้โปรดทราบว่าการขนานกันนั้นมักจะนำมาซึ่งลัทธิเอนโดเทมินิสม์ในการคำนวณที่มักจะถูกซ่อนไว้โดยการใช้งานแบบต่อเนื่อง บางครั้งสิ่งนี้ไม่สำคัญหรือสามารถบรรเทาได้ด้วยการ จำกัด การดำเนินการที่เกี่ยวข้อง (เช่นตัวดำเนินการลดต้องไร้สัญชาติและเชื่อมโยงกัน)

ในความเป็นจริงบางครั้งการขนานจะทำให้การคำนวณของคุณเร็วขึ้นบางครั้งก็ไม่ได้และบางครั้งมันก็จะทำให้ช้าลง ที่ดีที่สุดคือการพัฒนาครั้งแรกโดยใช้การดำเนินการตามลำดับแล้วใช้ขนานที่

(A) คุณรู้ว่ามีประโยชน์กับการเพิ่มประสิทธิภาพและ

(B) ว่าจะให้ประสิทธิภาพที่เพิ่มขึ้นจริง

(A) เป็นปัญหาทางธุรกิจไม่ใช่ปัญหาด้านเทคนิค หากคุณเป็นผู้เชี่ยวชาญด้านประสิทธิภาพคุณมักจะสามารถดูรหัสและตรวจสอบ (B) แต่เส้นทางสมาร์ทคือการวัด (และอย่ากังวลจนกว่าคุณจะมั่นใจได้ว่า (A) ถ้ารหัสนั้นเร็วพอแนะนำให้ใช้รอบสมองของคุณที่อื่นดีกว่า)

แบบจำลองประสิทธิภาพที่ง่ายที่สุดสำหรับการขนานคือโมเดล "NQ" โดยที่ N คือจำนวนองค์ประกอบและ Q คือการคำนวณต่อองค์ประกอบ โดยทั่วไปคุณต้องมีผลิตภัณฑ์ NQ เกินขีด จำกัด บางอย่างก่อนที่คุณจะเริ่มได้รับประโยชน์ด้านประสิทธิภาพ สำหรับปัญหาต่ำ Q เช่น "เพิ่มตัวเลขตั้งแต่ 1 ถึง N" คุณจะเห็นจุดคุ้มทุนระหว่าง N = 1,000 ถึง N = 10,000 ด้วยปัญหาถาม - ตอบสูงกว่าคุณจะเห็น breakevens ที่ระดับต่ำกว่า

แต่ความจริงแล้วค่อนข้างซับซ้อน ดังนั้นจนกว่าคุณจะประสบความสำเร็จในขั้นแรกให้ระบุก่อนว่าการประมวลผลแบบต่อเนื่องจะทำให้คุณต้องเสียค่าใช้จ่ายจริงหรือไม่


18
โพสต์นี้ให้รายละเอียดเพิ่มเติมเกี่ยวกับโมเดล NQ: gee.cs.oswego.edu/dl/html/StreamParallelGuidance.html
Pino

4
@specializt: การเปลี่ยนกระแสจากลำดับเป็นขนานจะเปลี่ยนอัลกอริทึม (ในกรณีส่วนใหญ่) การกำหนดระดับที่กล่าวถึงที่นี่เกี่ยวข้องกับคุณสมบัติของคุณ (โดยพลการ) ผู้ดำเนินการอาจพึ่งพา (การใช้งาน Stream ไม่สามารถรู้ได้) แต่แน่นอนไม่ควรพึ่งพา นั่นคือสิ่งที่ส่วนของคำตอบนี้พยายามที่จะพูด หากคุณใส่ใจกฎคุณสามารถมีผลลัพธ์ที่กำหนดขึ้นเช่นเดียวกับที่คุณพูด (มิฉะนั้นลำธารแบบขนานนั้นค่อนข้างไร้ประโยชน์) แต่ก็มีความเป็นไปได้ที่จะมีการอนุญาตโดยไม่ตั้งใจเช่นเมื่อใช้findAnyแทนfindFirst...
Holger

4
"ขั้นแรกให้สังเกตว่าการขนานนั้นไม่ได้มีประโยชน์อะไรนอกจากความเป็นไปได้ของการดำเนินการที่รวดเร็วกว่าเมื่อมีแกนประมวลผลเพิ่มเติม" - หรือหากคุณใช้การกระทำที่เกี่ยวข้องกับ IO (เช่น myListOfURLs.stream().map((url) -> downloadPage(url))...)
จูลส์

6
@Pacerier นั่นเป็นทฤษฎีที่ดี แต่ไร้เดียงสาเศร้า (ดูประวัติ 30 ปีของความพยายามในการสร้างคอมไพเลอร์แบบขนานอัตโนมัติเพื่อเริ่มต้น) เนื่องจากมันไม่สามารถคาดเดาได้อย่างถูกต้องพอที่จะไม่รบกวนผู้ใช้เมื่อเราหลีกเลี่ยงไม่ได้สิ่งที่ต้องทำคือเพียงแค่ให้ผู้ใช้พูดในสิ่งที่พวกเขาต้องการ สำหรับสถานการณ์ส่วนใหญ่ค่าเริ่มต้น (ลำดับ) นั้นถูกต้องและสามารถคาดเดาได้มากกว่า
Brian Goetz

2
@Jules: อย่าใช้สตรีมขนานสำหรับ IO มีไว้สำหรับการทำงานที่ต้องใช้ CPU สูง สตรีมแบบขนานใช้ForkJoinPool.commonPool()และคุณไม่ต้องการให้บล็อกงานไปที่นั่น
R2C2

68

ฉันดูหนึ่งในการนำเสนอผลงานของไบรอันเก๊ (Java Language สถาปนิกและสเปคนำสำหรับแลมบ์ดานิพจน์) เขาอธิบายในรายละเอียดเกี่ยวกับ 4 จุดต่อไปนี้ที่ต้องพิจารณาก่อนทำการขนาน

ค่าใช้จ่ายในการแยก / ย่อยสลาย
- บางครั้งการแยกมีราคาแพงกว่าเพียงแค่ทำงาน
ค่าใช้จ่ายในการจัดส่ง / การจัดการ
- สามารถทำงานได้มากในเวลาที่ใช้ในการส่งงานให้กับเธรดอื่น
ต้นทุนชุดค่าผสมผลลัพธ์
- บางครั้งชุดค่าผสมเกี่ยวข้องกับการคัดลอกข้อมูลจำนวนมาก ตัวอย่างเช่นการเพิ่มตัวเลขมีราคาถูกในขณะที่ชุดการรวมมีราคาแพง
สถานที่
- ช้างในห้อง นี่คือจุดสำคัญที่ทุกคนอาจพลาด คุณควรพิจารณาถึงการพลาดแคชหาก CPU รอข้อมูลเนื่องจากแคชหายไปดังนั้นคุณจะไม่ได้รับอะไรเลยจากการขนาน นั่นเป็นสาเหตุที่แหล่งที่มาของอาเรย์นั้นขนานที่ดีที่สุดเพราะดัชนีถัดไป (ใกล้กับดัชนีปัจจุบัน) ถูกแคชและมีโอกาสน้อยกว่าที่ CPU จะได้รับแคช

นอกจากนี้เขายังกล่าวถึงสูตรที่ค่อนข้างง่ายเพื่อกำหนดโอกาสในการเร่งความเร็วแบบขนาน

รุ่น NQ :

N x Q > 10000

ที่ไหน
N = จำนวนรายการข้อมูล
Q = จำนวนงานต่อรายการ


13

JB กระแทกเล็บที่หัว สิ่งเดียวที่ฉันสามารถเพิ่มคือ Java 8 ไม่ได้ทำประมวลผลแบบขนานบริสุทธิ์ก็ไม่paraquential ใช่ฉันเขียนบทความและฉันได้ทำ F / J มาสามสิบปีแล้วดังนั้นฉันจึงเข้าใจปัญหา


10
สตรีมไม่สามารถทำซ้ำได้เพราะสตรีมจะทำซ้ำภายในแทนภายนอก นั่นคือเหตุผลทั้งหมดสำหรับการสตรีมอย่างไรก็ตาม หากคุณมีปัญหากับงานวิชาการการเขียนโปรแกรมเพื่อการทำงานอาจไม่เหมาะกับคุณ การเขียนโปรแกรมเชิงหน้าที่ === คณิตศาสตร์ === ด้านวิชาการ และไม่ J8-FJ ไม่แตกมันเป็นเพียงว่าคนส่วนใหญ่ไม่อ่านคู่มือ f ****** เอกสาร java พูดชัดเจนมากว่าไม่ใช่กรอบการทำงานแบบขนาน นั่นคือเหตุผลทั้งหมดสำหรับสิ่งที่ spliterator ใช่มันเป็นวิชาการใช่มันใช้ได้ผลถ้าคุณรู้วิธีใช้ ใช่มันจะง่ายกว่าถ้าจะใช้ตัวจัดการแบบกำหนดเอง
Kr0e

1
สตรีมมีเมธอด iterator () ดังนั้นคุณสามารถทำซ้ำได้ถ้าคุณต้องการ ความเข้าใจของฉันคือว่าพวกเขาไม่ได้ใช้ Iterable เพราะคุณสามารถใช้ตัววนซ้ำนั้นเพียงครั้งเดียวและไม่มีใครสามารถตัดสินใจได้ว่ามันจะตกลง
Trejkaz

14
จะซื่อสัตย์: กระดาษทั้งหมดของคุณอ่านเหมือนขนาดใหญ่พูดจาโผงผางซับซ้อน - และที่สวยมากขัดแย้งกับความน่าเชื่อถือของ ... ผมอยากแนะนำให้ใหม่ทำมันมีมากแผ่วก้าวร้าวน้อยเป็นอย่างอื่นไม่ได้หลายคนจริงจะรำคาญที่จะอ่านมันอย่างเต็มที่ ... ฉันแค่พูดว่า
specializt

คำถามสองสามข้อเกี่ยวกับบทความของคุณ ... ก่อนอื่นทำไมคุณถึงเปรียบเสมือนโครงสร้างต้นไม้ที่สมดุลด้วยกราฟ acyclic กำกับ? ใช่ต้นไม้ที่สมดุลเป็น DAG แต่เป็นลิสต์ที่เชื่อมโยงกันและโครงสร้างข้อมูลเชิงวัตถุทุกตัวที่นอกเหนือจากอาร์เรย์ นอกจากนี้เมื่อคุณบอกว่าการย่อยสลายแบบเรียกซ้ำจะทำงานเฉพาะกับโครงสร้างต้นไม้ที่สมดุลและดังนั้นจึงไม่เกี่ยวข้องในเชิงพาณิชย์คุณจะยืนยันการยืนยันดังกล่าวอย่างไร ดูเหมือนว่าสำหรับฉัน (เป็นที่ยอมรับโดยไม่ตรวจสอบปัญหาในเชิงลึกจริงๆ) ว่าควรทำงานได้ดีเช่นเดียวกับโครงสร้างฐานข้อมูลแบบอาเรย์เช่นArrayList/HashMap /
จูลส์

1
กระทู้นี้มาจากปี 2013 มีการเปลี่ยนแปลงมากมายตั้งแต่นั้นมา ส่วนนี้มีไว้สำหรับความคิดเห็นที่ไม่มีคำตอบโดยละเอียด
เผยแพร่เมื่อ

3

คำตอบอื่น ๆ ได้รวมถึงการทำโปรไฟล์เพื่อหลีกเลี่ยงการปรับให้เหมาะสมก่อนเวลาอันควรและค่าใช้จ่ายในการประมวลผลแบบขนาน คำตอบนี้อธิบายถึงตัวเลือกที่ดีที่สุดของโครงสร้างข้อมูลสำหรับการสตรีมแบบขนาน

ตามกฎแล้วประสิทธิภาพที่เพิ่มขึ้นจากการขนานจะดีที่สุดในการสตรีม ArrayList , HashMap, HashSetและConcurrentHashMapอินสแตนซ์; อาร์เรย์; intช่วง; และlongช่วง สิ่งที่โครงสร้างข้อมูลเหล่านี้มีเหมือนกันคือพวกมันสามารถแบ่งได้อย่างถูกต้องและถูกเป็น subranges ของขนาดที่ต้องการใด ๆ ซึ่งทำให้ง่ายต่อการแบ่งงานระหว่างเธรดแบบขนาน สิ่งที่เป็นนามธรรมที่ใช้โดยไลบรารีของสตรีมเพื่อทำงานนี้คือตัวแยกซึ่งถูกส่งคืนโดยspliteratorเมธอด on StreamและIterableและ

ปัจจัยสำคัญอีกประการที่โครงสร้างข้อมูลเหล่านี้มีเหมือนกันคือให้การอ้างอิงที่ดีถึงยอดเยี่ยมเมื่อประมวลผลตามลำดับการอ้างอิงองค์ประกอบตามลำดับจะถูกเก็บไว้ในหน่วยความจำ วัตถุที่อ้างถึงโดยการอ้างอิงเหล่านั้นอาจไม่ใกล้เคียงกันในหน่วยความจำซึ่งจะช่วยลดการอ้างอิงท้องถิ่น Local-of-reference กลายเป็นสิ่งสำคัญอย่างยิ่งสำหรับการดำเนินการแบบขนานจำนวนมาก: โดยไม่ต้องเธรดจะใช้เวลาส่วนใหญ่ในการรอข้อมูลที่จะถ่ายโอนจากหน่วยความจำไปยังแคชของโปรเซสเซอร์ โครงสร้างข้อมูลที่มีตำแหน่งอ้างอิงที่ดีที่สุดคืออาร์เรย์ดั้งเดิมเนื่องจากข้อมูลนั้นถูกเก็บไว้ในหน่วยความจำอย่างต่อเนื่อง

ที่มา: ข้อ # 48 ใช้ความระมัดระวังเมื่อทำการสตรีมแบบขนาน, Java 3e ที่มีประสิทธิภาพโดย Joshua Bloch


2

อย่าขนานกระแสข้อมูลที่ไม่มีที่สิ้นสุดด้วยขีด จำกัด นี่คือสิ่งที่เกิดขึ้น:

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

ผลลัพธ์

    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 ...

เหมือนกันถ้าคุณใช้ .limit(...)

คำอธิบายที่นี่: Java 8 การใช้. Parallel ในสตรีมทำให้เกิดข้อผิดพลาด OOM

ในทำนองเดียวกันอย่าใช้ขนานหากมีการสั่งซื้อสตรีมและมีองค์ประกอบมากกว่าที่คุณต้องการประมวลผลเช่น

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

สิ่งนี้อาจทำงานได้นานขึ้นเนื่องจากเธรดแบบขนานอาจทำงานได้ในช่วงจำนวนมากมายแทนที่จะเป็นสิ่งสำคัญ 0-100 ทำให้สิ่งนี้ใช้เวลานานมาก

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.