Các luồng Java vô hạn song song hết bộ nhớ


16

Tôi đang cố gắng để hiểu tại sao chương trình Java sau cung cấp một OutOfMemoryError, trong khi chương trình tương ứng mà không .parallel()có.

System.out.println(Stream
    .iterate(1, i -> i+1)
    .parallel()
    .flatMap(n -> Stream.iterate(n, i -> i+n))
    .mapToInt(Integer::intValue)
    .limit(100_000_000)
    .sum()
);

Tôi có hai câu hỏi:

  1. Đầu ra dự định của chương trình này là gì?

    Không có .parallel()vẻ như điều này chỉ đơn giản là đầu ra sum(1+2+3+...)có nghĩa là nó chỉ đơn giản là "bị kẹt" ở luồng đầu tiên trong FlatMap, điều này có ý nghĩa.

    Song song tôi không biết liệu có một hành vi dự kiến ​​hay không, nhưng tôi đoán là bằng cách nào đó đã xen kẽ các nluồng đầu tiên hoặc lâu hơn, trong đó nsố lượng công nhân song song. Nó cũng có thể hơi khác nhau dựa trên hành vi chunking / đệm.

  2. Điều gì gây ra nó hết bộ nhớ? Tôi đặc biệt đang cố gắng hiểu làm thế nào các luồng này được thực hiện dưới mui xe.

    Tôi đoán thứ gì đó chặn luồng, vì vậy nó không bao giờ kết thúc và có thể loại bỏ các giá trị được tạo, nhưng tôi không biết thứ tự nào được đánh giá và nơi đệm xảy ra.

Chỉnh sửa: Trong trường hợp có liên quan, tôi đang sử dụng Java 11.

Editt 2: Rõ ràng điều tương tự xảy ra ngay cả đối với chương trình đơn giản IntStream.iterate(1,i->i+1).limit(1000_000_000).parallel().sum(), vì vậy nó có thể phải làm với sự lười biếng limithơn là flatMap.


song song () trong nội bộ sử dụng ForkJoinPool. Tôi đoán ForkJoin Framework là trong Java từ Java 7
Aravind

Câu trả lời:


9

Bạn nói rằng nhưng tôi không biết thứ tự nào được đánh giá và việc đệm xảy ra ở đâu , đó chính xác là những gì các luồng song song nói về. Thứ tự đánh giá là không xác định.

Một khía cạnh quan trọng của ví dụ của bạn là .limit(100_000_000). Điều này ngụ ý rằng việc triển khai không thể chỉ tổng hợp các giá trị tùy ý, mà phải tổng hợp 100.000.000 số đầu tiên . Lưu ý rằng trong triển khai tham chiếu, .unordered().limit(100_000_000)không thay đổi kết quả, điều này cho thấy rằng không có triển khai đặc biệt nào cho trường hợp không có thứ tự, nhưng đó là một chi tiết thực hiện.

Bây giờ, khi các luồng công nhân xử lý các phần tử, chúng không thể tổng hợp chúng, vì chúng phải biết phần tử nào được phép tiêu thụ, điều này phụ thuộc vào số lượng phần tử trước khối lượng công việc cụ thể của chúng. Vì luồng này không biết kích thước, nên chỉ có thể biết điều này khi các phần tử tiền tố đã được xử lý, điều này không bao giờ xảy ra đối với các luồng vô hạn. Vì vậy, các chủ đề công nhân tiếp tục đệm trong thời điểm này, thông tin này sẽ có sẵn.

Về nguyên tắc, khi một luồng công nhân biết rằng nó xử lý khối công việc ngoài cùng bên trái, nó có thể tổng hợp các phần tử ngay lập tức, đếm chúng và báo hiệu kết thúc khi đạt đến giới hạn. Vì vậy, Stream có thể chấm dứt, nhưng điều này phụ thuộc vào rất nhiều yếu tố.

Trong trường hợp của bạn, một kịch bản hợp lý là các luồng công nhân khác nhanh hơn trong việc phân bổ bộ đệm so với công việc ngoài cùng bên trái đang tính. Trong kịch bản này, các thay đổi tinh vi về thời gian có thể khiến luồng đôi khi trở lại với một giá trị.

Khi chúng ta làm chậm tất cả các luồng công nhân trừ một luồng xử lý đoạn ngoài cùng bên trái, chúng ta có thể làm cho luồng kết thúc (ít nhất là trong hầu hết các lần chạy):

System.out.println(IntStream
    .iterate(1, i -> i+1)
    .parallel()
    .peek(i -> { if(i != 1) LockSupport.parkNanos(1_000_000_000); })
    .flatMap(n -> IntStream.iterate(n, i -> i+n))
    .limit(100_000_000)
    .sum()
);

Tôi đang làm theo gợi ý của Stuart Marks để sử dụng thứ tự từ trái sang phải khi nói về thứ tự gặp gỡ hơn là thứ tự xử lý.


Câu trả lời rất hay! Tôi tự hỏi liệu có nguy cơ rằng tất cả các luồng bắt đầu chạy các hoạt động FlatMap không, và không có phân bổ nào để thực sự làm trống bộ đệm (tính tổng)? Trong trường hợp sử dụng thực tế của tôi, các luồng vô hạn thay vì các tệp quá lớn để giữ trong bộ nhớ. Tôi tự hỏi làm thế nào tôi có thể viết lại luồng để giảm mức sử dụng bộ nhớ?
Thomas Ahle

1
Bạn đang sử dụng Files.lines(…)? Nó đã được cải thiện đáng kể trong Java 9.
Holger

1
Đây là những gì nó làm trong Java 8. Trong các JRE mới hơn, nó vẫn sẽ quay trở lại BufferedReader.lines()trong một số trường hợp nhất định (không phải hệ thống tệp mặc định, bộ ký tự đặc biệt hoặc kích thước lớn hơn Integer.MAX_FILES). Nếu một trong những điều này được áp dụng, một giải pháp tùy chỉnh có thể giúp ích. Điều này sẽ có giá trị một Q & A mới
Holger

1
Integer.MAX_VALUE, tất nhiên là đào
Holger

1
Luồng ngoài, luồng tập tin là gì? Liệu nó có một kích thước dự đoán?
Holger

5

Đoán tốt nhất của tôi là thêm parallel()thay đổi hành vi nội bộ flatMap()vấn đề đã có được đánh giá một cách lười biếng trước .

Các OutOfMemoryErrorlỗi mà bạn đang nhận được báo cáo trong [JDK-8202307] Bắt một java.lang.OutOfMemoryError:. Không gian đống Java khi gọi Stream.iterator () bên cạnh () trên một dòng suối trong đó sử dụng một vô hạn / Suối rất lớn trong flatMap . Nếu bạn nhìn vào vé, nó sẽ ít nhiều giống với dấu vết ngăn xếp mà bạn đang nhận được. Vé đã bị đóng vì không sửa với lý do sau:

Các phương thức iterator()spliterator()"các cửa thoát hiểm" sẽ được sử dụng khi không thể sử dụng các hoạt động khác. Chúng có một số hạn chế vì chúng biến mô hình đẩy của việc triển khai luồng thành mô hình kéo. Quá trình chuyển đổi như vậy đòi hỏi phải có bộ đệm trong một số trường hợp nhất định, chẳng hạn như khi một phần tử (phẳng) được ánh xạ tới hai hoặc nhiều phần tử . Nó sẽ làm phức tạp đáng kể việc thực hiện luồng, có thể phải trả giá bằng các trường hợp phổ biến, để hỗ trợ một khái niệm về áp suất ngược để truyền đạt bao nhiêu phần tử để kéo qua các lớp sản xuất phần tử lồng nhau.


Điều này rất thú vị! Nó có ý nghĩa rằng quá trình chuyển đổi đẩy / kéo đòi hỏi bộ đệm có thể sử dụng hết bộ nhớ. Tuy nhiên trong trường hợp của tôi, có vẻ như chỉ sử dụng đẩy nên hoạt động tốt và chỉ cần loại bỏ các yếu tố còn lại khi chúng xuất hiện? Hoặc có thể bạn đang nói rằng flapmap khiến một trình vòng lặp được tạo ra?
Thomas Ahle

3

OOME được gây ra không phải bởi luồng là vô hạn, mà thực tế là nó không phải là .

Tức là, nếu bạn bình luận .limit(...), nó sẽ không bao giờ hết bộ nhớ - nhưng tất nhiên, nó cũng sẽ không bao giờ kết thúc.

Sau khi phân tách, luồng chỉ có thể theo dõi số lượng phần tử nếu chúng được tích lũy trong mỗi luồng (trông giống như bộ tích lũy thực tế Spliterators$ArraySpliterator#array).

Có vẻ như bạn có thể sao chép nó mà không cần flatMap, chỉ cần chạy như sau với -Xmx128m:

    System.out.println(Stream
            .iterate(1, i -> i + 1)
            .parallel()
      //    .flatMap(n -> Stream.iterate(n, i -> i+n))
            .mapToInt(Integer::intValue)
            .limit(100_000_000)
            .sum()
    );

Tuy nhiên, sau khi bình luận limit(), nó sẽ chạy tốt cho đến khi bạn quyết định sử dụng máy tính xách tay của mình.

Bên cạnh các chi tiết triển khai thực tế, đây là những gì tôi nghĩ đang xảy ra:

Với limit, bộ sumgiảm muốn các phần tử X đầu tiên tổng hợp, vì vậy không có luồng nào có thể phát ra tổng một phần. Mỗi "lát" (luồng) sẽ cần tích lũy các phần tử và chuyển chúng qua. Không có giới hạn, không có ràng buộc nào như vậy nên mỗi "lát" sẽ chỉ tính tổng một phần trong số các phần tử mà nó nhận được (mãi mãi), giả sử cuối cùng nó sẽ phát ra kết quả.


Bạn có ý nghĩa gì "một khi nó bị chia tách"? Có giới hạn phân chia nó bằng cách nào đó?
Thomas Ahle

@ThomasAhle parallel()sẽ sử dụng ForkJoinPoolnội bộ để đạt được sự song song. Ý Spliteratorchí sẽ được sử dụng để phân công công việc cho từng ForkJoinnhiệm vụ, tôi đoán chúng ta có thể gọi đơn vị công việc ở đây là "split".
Karol Dowbecki

Nhưng tại sao điều đó chỉ xảy ra với giới hạn?
Thomas Ahle

@ThomasAhle Tôi đã chỉnh sửa câu trả lời bằng hai xu của mình.
Costi Ciudatu

1
@ThomasAhle đặt điểm dừng trong Integer.sum(), được sử dụng bởi bộ IntStream.sumgiảm tốc. Bạn sẽ thấy rằng phiên bản không giới hạn gọi chức năng đó mọi lúc, trong khi phiên bản giới hạn không bao giờ được gọi trước OOM.
Costi Ciudatu
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.