Tôi có nên luôn luôn sử dụng một luồng song song khi có thể?


514

Với Java 8 và lambdas, việc lặp lại các bộ sưu tập dưới dạng luồng và dễ dàng sử dụng một luồng song song. Hai ví dụ từ các tài liệu , ví dụ thứ hai sử dụngallelStream:

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

Miễn là tôi không quan tâm đến thứ tự, việc sử dụng song song có ích không? Người ta sẽ nghĩ rằng việc phân chia công việc trên nhiều lõi nhanh hơn.

Có những cân nhắc khác? Khi nào nên sử dụng luồng song song và khi nào không nên sử dụng song song?

(Câu hỏi này được yêu cầu kích hoạt một cuộc thảo luận về cách thức và thời điểm sử dụng các luồng song song, không phải vì tôi nghĩ luôn luôn sử dụng chúng là một ý tưởng hay.)

Câu trả lời:


735

Luồng song song có tổng phí cao hơn nhiều so với luồng liên tiếp. Phối hợp các chủ đề mất một lượng thời gian đáng kể. Tôi sẽ sử dụng các luồng tuần tự theo mặc định và chỉ xem xét các luồng song song nếu

  • Tôi có một lượng lớn vật phẩm cần xử lý (hoặc việc xử lý từng vật phẩm cần có thời gian và có thể song song hóa)

  • Tôi có một vấn đề hiệu suất ở nơi đầu tiên

  • Tôi chưa chạy quy trình trong môi trường đa luồng (ví dụ: trong bộ chứa web, nếu tôi có nhiều yêu cầu xử lý song song, việc thêm một lớp song song bên trong mỗi yêu cầu có thể có nhiều tiêu cực hơn hiệu ứng tích cực )

Trong ví dụ của bạn, hiệu suất sẽ được điều khiển bởi quyền truy cập được đồng bộ hóa System.out.println()và làm cho quá trình này song song sẽ không có hiệu lực, hoặc thậm chí là tiêu cực.

Ngoài ra, hãy nhớ rằng các luồng song song không giải quyết một cách kỳ diệu tất cả các vấn đề đồng bộ hóa. Nếu tài nguyên được chia sẻ được sử dụng bởi các vị từ và hàm được sử dụng trong quy trình, bạn sẽ phải đảm bảo rằng mọi thứ đều an toàn theo luồng. Đặc biệt, tác dụng phụ là những điều bạn thực sự phải lo lắng nếu đi song song.

Trong mọi trường hợp, đo lường, đừng đoán! Chỉ một phép đo sẽ cho bạn biết liệu sự song song có đáng hay không.


18
Câu trả lời tốt. Tôi sẽ nói thêm rằng nếu bạn có một lượng lớn vật phẩm cần xử lý, điều đó chỉ làm tăng các vấn đề phối hợp luồng; Chỉ khi xử lý từng mục cần có thời gian và có thể song song hóa việc song song hóa có thể hữu ích.
Warren Dew

16
@WarrenDew tôi không đồng ý. Hệ thống Fork / Tham gia đơn giản sẽ chia N mục thành, ví dụ, 4 phần và xử lý 4 phần này theo tuần tự. 4 kết quả sau đó sẽ được giảm. Nếu lớn thực sự là rất lớn, ngay cả để xử lý đơn vị nhanh, song song có thể có hiệu quả. Nhưng như mọi khi, bạn phải đo.
JB Nizet

Tôi có một bộ sưu tập các đối tượng triển khai Runnablemà tôi gọi start()để sử dụng chúng Threads, liệu có thể thay đổi điều đó thành sử dụng các luồng java 8 theo cách .forEach()song song không? Sau đó, tôi sẽ có thể loại bỏ mã chủ đề ra khỏi lớp. Nhưng có bất kỳ nhược điểm?
ycomp

1
@JBNizet Nếu 4 phần pocess tuần tự, thì không có sự khác biệt của nó là song song quá trình hay tuần tự biết? Xin làm rõ
Harshana

3
@Harshana rõ ràng có nghĩa là các yếu tố của mỗi trong 4 phần sẽ được xử lý tuần tự. Tuy nhiên, các bộ phận có thể được xử lý đồng thời. Nói cách khác, nếu bạn có sẵn một số lõi CPU, mỗi phần có thể chạy trên lõi riêng độc lập với các phần khác, trong khi xử lý các phần tử của chính nó theo tuần tự. (LƯU Ý: Tôi không biết, nếu đây là cách các luồng Java song song hoạt động, tôi chỉ đang cố gắng làm rõ ý nghĩa của JBNizet.)
ngày mai

258

API Stream được thiết kế để giúp dễ dàng viết các tính toán theo cách được trừu tượng hóa khỏi cách chúng sẽ được thực thi, giúp chuyển đổi giữa tuần tự và song song dễ dàng.

Tuy nhiên, chỉ vì nó dễ dàng, không có nghĩa là nó luôn luôn là một ý tưởng tốt, và trên thực tế, đó là một ý tưởng tồi khi chỉ cần bỏ .parallel()đi khắp nơi chỉ vì bạn có thể.

Đầu tiên, lưu ý rằng tính song song không mang lại lợi ích nào ngoài khả năng thực thi nhanh hơn khi có nhiều lõi hơn. Một thực thi song song sẽ luôn liên quan đến nhiều công việc hơn một công việc tuần tự, bởi vì ngoài việc giải quyết vấn đề, nó còn phải thực hiện việc gửi và điều phối các nhiệm vụ phụ. Hy vọng là bạn sẽ có thể có được câu trả lời nhanh hơn bằng cách chia nhỏ công việc trên nhiều bộ xử lý; việc này có thực sự xảy ra hay không phụ thuộc vào rất nhiều thứ, bao gồm kích thước của tập dữ liệu của bạn, mức độ tính toán của bạn trên mỗi phần tử, tính chất của tính toán (cụ thể là việc xử lý một phần tử có tương tác với xử lý của phần tử khác không?) , số lượng bộ xử lý có sẵn và số lượng các tác vụ khác cạnh tranh cho các bộ xử lý đó.

Hơn nữa, lưu ý rằng song song cũng thường phơi bày chủ nghĩa không xác định trong tính toán thường bị ẩn bởi các triển khai tuần tự; đôi khi điều này không thành vấn đề, hoặc có thể được giảm thiểu bằng cách hạn chế các hoạt động liên quan (nghĩa là, các toán tử giảm phải không trạng thái và liên kết.)

Trong thực tế, đôi khi song song sẽ tăng tốc tính toán của bạn, đôi khi nó sẽ không, và đôi khi nó sẽ làm chậm nó. Tốt nhất là phát triển đầu tiên bằng cách sử dụng thực thi tuần tự và sau đó áp dụng song song trong đó

(A) bạn biết rằng thực sự có lợi cho việc tăng hiệu suất và

(B) rằng nó thực sự sẽ cung cấp hiệu suất tăng.

(A) là một vấn đề kinh doanh, không phải là một vấn đề kỹ thuật. Nếu bạn là một chuyên gia về hiệu suất, bạn thường sẽ có thể xem mã và xác định (B), nhưng con đường thông minh là để đo lường. (Và, thậm chí đừng bận tâm cho đến khi bạn bị thuyết phục (A); nếu mã đủ nhanh, tốt hơn để áp dụng chu kỳ não của bạn ở nơi khác.)

Mô hình hiệu suất đơn giản nhất cho tính song song là mô hình "NQ", trong đó N là số phần tử và Q là tính toán trên mỗi phần tử. Nói chung, bạn cần NQ sản phẩm vượt quá một số ngưỡng trước khi bạn bắt đầu nhận được lợi ích hiệu suất. Đối với một vấn đề Q thấp như "cộng các số từ 1 đến N", bạn thường sẽ thấy mức hòa vốn giữa N = 1000 và N = 10000. Với các vấn đề Q cao hơn, bạn sẽ thấy breakevens ở ngưỡng thấp hơn.

Nhưng thực tế khá phức tạp. Vì vậy, cho đến khi bạn đạt được kinh nghiệm, trước tiên hãy xác định khi nào việc xử lý tuần tự thực sự khiến bạn phải trả giá, và sau đó đo lường xem liệu song song có giúp ích gì không.


18
Bài đăng này cung cấp thêm chi tiết về mô hình NQ: gee.cs.oswego.edu/dl/html/StreamParallelGuidance.html
Pino

4
@specializt: chuyển một luồng từ tuần tự sang song song không làm thay đổi thuật toán (trong hầu hết các trường hợp). Tính xác định được đề cập ở đây liên quan đến các thuộc tính mà các nhà khai thác (tùy ý) của bạn có thể dựa vào (việc triển khai Luồng không thể biết điều đó), nhưng tất nhiên không nên dựa vào. Đó là những gì phần của câu trả lời này đã cố gắng nói. Nếu bạn quan tâm đến các quy tắc, bạn có thể có một kết quả xác định, giống như bạn nói, (suối khác song song khá vô dụng), nhưng cũng có khả năng cho phép cố ý không thuyết định mệnh, giống như khi sử dụng findAnythay vì findFirst...
Holger

4
"Đầu tiên, lưu ý rằng tính song song không mang lại lợi ích nào ngoài khả năng thực thi nhanh hơn khi có nhiều lõi hơn" - hoặc nếu bạn đang áp dụng một hành động liên quan đến IO (ví dụ myListOfURLs.stream().map((url) -> downloadPage(url))...).
Jules

6
@Pacerier Đó là một lý thuyết hay, nhưng thật ngây thơ (xem lịch sử 30 năm cố gắng xây dựng trình biên dịch song song tự động để bắt đầu). Vì nó không thực tế để đoán đúng thời gian để không gây khó chịu cho người dùng khi chúng tôi chắc chắn đã hiểu sai, điều có trách nhiệm phải làm là chỉ để người dùng nói những gì họ muốn. Đối với hầu hết các tình huống, mặc định (tuần tự) là đúng và dễ dự đoán hơn.
Brian Goetz

2
@Jules: Không bao giờ sử dụng các luồng song song cho IO. Chúng chỉ có ý nghĩa cho các hoạt động chuyên sâu CPU. Luồng song song sử dụng ForkJoinPool.commonPool()và bạn không muốn chặn các tác vụ đến đó.
R2C2

68

Tôi đã xem một trong các bài thuyết trình của Brian Goetz (Kiến trúc sư ngôn ngữ Java & trưởng nhóm đặc tả cho Lambda Expressions) . Ông giải thích chi tiết 4 điểm sau đây để xem xét trước khi đi song song:

Chi phí tách / phân tách
- Đôi khi chia tách tốn kém hơn so với chỉ thực hiện công việc!
Nhiệm vụ quản lý / chi phí quản lý
- Có thể thực hiện rất nhiều công việc trong thời gian cần thiết để bàn giao công việc cho một chủ đề khác.
Chi phí kết hợp kết quả
- Đôi khi kết hợp liên quan đến việc sao chép nhiều dữ liệu. Ví dụ, thêm số là rẻ trong khi các bộ hợp nhất là đắt.
Địa phương
- Con voi trong phòng. Đây là một điểm quan trọng mà mọi người có thể bỏ lỡ. Bạn nên xem xét các lỗi bộ nhớ cache, nếu CPU chờ dữ liệu vì bộ nhớ cache bị mất thì bạn sẽ không đạt được bất cứ điều gì bằng cách song song hóa. Đó là lý do tại sao các nguồn dựa trên mảng song song tốt nhất khi các chỉ số tiếp theo (gần chỉ mục hiện tại) được lưu vào bộ đệm và có ít khả năng CPU sẽ gặp lỗi bộ nhớ cache.

Ông cũng đề cập đến một công thức tương đối đơn giản để xác định cơ hội tăng tốc song song.

Mô hình NQ :

N x Q > 10000

Trong đó,
N = số mục dữ liệu
Q = số lượng công việc trên mỗi mục


13

JB đánh vào đầu đinh. Điều duy nhất tôi có thể thêm là Java 8 không thực hiện xử lý song song thuần túy, nó không có giá trị . Có, tôi đã viết bài báo và tôi đã làm F / J trong ba mươi năm vì vậy tôi hiểu vấn đề này.


10
Các luồng không lặp được vì các luồng thực hiện lặp bên trong thay vì bên ngoài. Đó là toàn bộ lý do cho các luồng. Nếu bạn gặp vấn đề với công việc học tập thì lập trình chức năng có thể không dành cho bạn. Lập trình hàm === toán === học thuật. Và không, J8-FJ không bị hỏng, chỉ là hầu hết mọi người không đọc hướng dẫn sử dụng f ******. Các tài liệu java nói rất rõ ràng rằng nó không phải là một khung thực thi song song. Đó là toàn bộ lý do cho tất cả các công cụ spliterator. Vâng, nó mang tính học thuật, có, nó hoạt động nếu bạn biết cách sử dụng nó. Có, sẽ dễ dàng hơn khi sử dụng trình thực thi tùy chỉnh
Kr0e

1
Stream không có phương thức iterator (), vì vậy bạn có thể lặp lại chúng bên ngoài nếu bạn muốn. Tôi hiểu rằng họ không thực hiện Iterable vì bạn chỉ có thể sử dụng trình lặp đó một lần và không ai có thể quyết định liệu điều đó có ổn không.
Trejkaz

14
phải trung thực: toàn bộ giấy của bạn đọc như một rant xây dựng khổng lồ - và đó là khá nhiều phủ nhận uy tín của mình ... tôi muốn khuyên bạn nên tái làm nó với một nhiều nhạt ít hung hăng khác không có nhiều người sẽ thực sự bận tâm để đọc đầy đủ nó ... tôi chỉ nói
đặc biệt

Một vài câu hỏi về bài viết của bạn ... trước hết, tại sao bạn lại đánh đồng các cấu trúc cây cân bằng với các biểu đồ chu kỳ có hướng? Đúng, cây cân bằng DAG, nhưng các danh sách được liên kết cũng vậy và hầu hết mọi cấu trúc dữ liệu hướng đối tượng khác với mảng. Ngoài ra, khi bạn nói phân rã đệ quy chỉ hoạt động trên các cấu trúc cây cân bằng và do đó không liên quan về mặt thương mại, làm thế nào để bạn biện minh cho khẳng định đó? Dường như với tôi (thừa nhận mà không thực sự kiểm tra vấn đề một cách sâu sắc) rằng nó sẽ hoạt động tốt như trên các cơ sở dữ liệu dựa trên mảng, ví dụ ArrayList/ HashMap.
Jules

1
Chủ đề này là từ năm 2013, rất nhiều thay đổi kể từ đó. Phần này là cho ý kiến ​​không trả lời chi tiết.
xuất hiện vào

3

Các câu trả lời khác đã được trình bày hồ sơ để tránh tối ưu hóa sớm và chi phí đầu tư trong xử lý song song. Câu trả lời này giải thích sự lựa chọn lý tưởng của các cấu trúc dữ liệu để truyền phát song song.

Như một quy luật, tăng hiệu suất từ xử lý song song là tốt nhất về con suối qua ArrayList, HashMap, HashSet, và ConcurrentHashMaptrường hợp; mảng; intcác dãy; và longphạm vi. Điểm chung của các cấu trúc dữ liệu này là tất cả chúng có thể được phân chia chính xác và rẻ tiền thành các phần phụ có kích thước mong muốn, giúp dễ dàng phân chia công việc giữa các luồng song song. Sự trừu tượng hóa được sử dụng bởi thư viện stream để thực hiện tác vụ này là bộ chia, được trả về bởi spliteratorphương thức trên StreamIterable.

Một yếu tố quan trọng khác mà tất cả các cấu trúc dữ liệu này có điểm chung là chúng cung cấp địa phương tham chiếu từ tốt đến xuất sắc khi được xử lý tuần tự: các tham chiếu phần tử tuần tự được lưu trữ cùng nhau trong bộ nhớ. Các đối tượng được tham chiếu bởi các tham chiếu đó có thể không gần nhau trong bộ nhớ, điều này làm giảm tính tham chiếu cục bộ. Tham chiếu địa phương hóa ra cực kỳ quan trọng để song song hóa các hoạt động hàng loạt: không có nó, các luồng dành nhiều thời gian nhàn rỗi, chờ dữ liệu được chuyển từ bộ nhớ vào bộ đệm của bộ xử lý. Các cấu trúc dữ liệu với vị trí tham chiếu tốt nhất là các mảng nguyên thủy vì bản thân dữ liệu được lưu trữ liên tục trong bộ nhớ.

Nguồn: Mục # 48 Sử dụng thận trọng khi tạo luồng song song, Java 3e hiệu quả của Joshua Bloch


2

Không bao giờ song song một luồng vô hạn với một giới hạn. Đây là những gì xảy ra:

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

Kết quả

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

Tương tự nếu bạn sử dụng .limit(...)

Giải thích ở đây: Java 8, sử dụng .pool trong luồng gây ra lỗi OOM

Tương tự, không sử dụng song song nếu luồng được sắp xếp và có nhiều phần tử hơn bạn muốn xử lý, ví dụ:

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

Điều này có thể chạy lâu hơn vì các luồng song song có thể hoạt động trên nhiều phạm vi số thay vì mức quan trọng 0-100, khiến việc này mất nhiều thời gian.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.