Nhóm luồng tùy chỉnh trong luồng song song Java 8


398

Có thể chỉ định nhóm luồng tùy chỉnh cho luồng song song Java 8 không? Tôi không thể tìm thấy nó ở bất cứ đâu.

Hãy tưởng tượng rằng tôi có một ứng dụng máy chủ và tôi muốn sử dụng các luồng song song. Nhưng ứng dụng này lớn và đa luồng nên tôi muốn ngăn cách nó. Tôi không muốn một tác vụ chạy chậm trong một mô-đun của các nhiệm vụ chặn ứng dụng từ một mô-đun khác.

Nếu tôi không thể sử dụng các nhóm luồng khác nhau cho các mô-đun khác nhau, điều đó có nghĩa là tôi không thể sử dụng các luồng song song một cách an toàn trong hầu hết các tình huống trong thế giới thực.

Hãy thử ví dụ sau đây. Có một số nhiệm vụ chuyên sâu CPU được thực hiện trong các luồng riêng biệt. Các nhiệm vụ tận dụng các luồng song song. Tác vụ đầu tiên bị hỏng, vì vậy mỗi bước mất 1 giây (mô phỏng theo chế độ ngủ của luồng). Vấn đề là các luồng khác bị kẹt và chờ đợi nhiệm vụ bị hỏng hoàn thành. Đây là ví dụ giả định, nhưng hãy tưởng tượng một ứng dụng servlet và ai đó gửi một tác vụ chạy dài đến nhóm tham gia fork chung.

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

3
Bạn có ý nghĩa gì bởi nhóm chủ đề tùy chỉnh? Có một ForkJoinPool chung duy nhất nhưng bạn luôn có thể tạo ForkJoinPool của riêng mình và gửi yêu cầu tới nó.
xuất hiện vào

7
Gợi ý: Nhà vô địch Java Heinz Kabutz kiểm tra vấn đề tương tự nhưng với tác động thậm chí còn tồi tệ hơn: Các chủ đề bế tắc của nhóm tham gia ngã ba chung. Xem javaspecialists.eu/archive/Issue223.html
Peti

Câu trả lời:


395

Thực sự có một mẹo làm thế nào để thực hiện một thao tác song song trong một nhóm kết nối cụ thể. Nếu bạn thực hiện nó như một nhiệm vụ trong nhóm tham gia ngã ba, nó sẽ ở đó và không sử dụng chung.

final int parallelism = 4;
ForkJoinPool forkJoinPool = null;
try {
    forkJoinPool = new ForkJoinPool(parallelism);
    final List<Integer> primes = forkJoinPool.submit(() ->
        // Parallel task here, for example
        IntStream.range(1, 1_000_000).parallel()
                .filter(PrimesPrint::isPrime)
                .boxed().collect(Collectors.toList())
    ).get();
    System.out.println(primes);
} catch (InterruptedException | ExecutionException e) {
    throw new RuntimeException(e);
} finally {
    if (forkJoinPool != null) {
        forkJoinPool.shutdown();
    }
}

Thủ thuật này dựa trên ForkJoinTask.fork , trong đó chỉ định: "Sắp xếp để thực hiện không đồng bộ tác vụ này trong nhóm mà tác vụ hiện tại đang chạy, nếu có thể hoặc sử dụng ForkJoinPool.commonPool () nếu không phải trongForkJoinPool ()"


20
Chi tiết về giải pháp được mô tả tại đây blog.krecan.net/2014/03/18/ tàng
Lukas

3
Nhưng nó cũng được chỉ định rằng các luồng sử dụng ForkJoinPoolhay đó là một chi tiết thực hiện? Một liên kết đến các tài liệu sẽ được tốt đẹp.
Nicolai

6
@Lukas Cảm ơn đoạn trích. Tôi sẽ thêm rằng ForkJoinPooltrường hợp đó là shutdown()khi không cần thiết nữa để tránh rò rỉ chủ đề. (ví dụ)
jck 18/2/2015

5
Lưu ý rằng có một lỗi trong Java 8, mặc dù các tác vụ đang chạy trên một cá thể nhóm tùy chỉnh, chúng vẫn được ghép nối với nhóm chung: kích thước của tính toán vẫn tỷ lệ với nhóm chung chứ không phải nhóm chung tùy chỉnh. Đã được sửa trong Java 10: JDK-8190974
Terran

3
@terran Vấn đề này cũng đã được sửa cho lỗi Java 8.openjdk.java.net/browse/JDK-8224620
Cutberto Ocampo

192

Các luồng song song sử dụng mặc định ForkJoinPool.commonPooltheo mặc định có một luồng ít hơn khi bạn có bộ xử lý , khi được trả về Runtime.getRuntime().availableProcessors()(Điều này có nghĩa là luồng song song sử dụng tất cả bộ xử lý của bạn vì chúng cũng sử dụng luồng chính):

Đối với các ứng dụng yêu cầu nhóm riêng hoặc nhóm tùy chỉnh, ForkJoinPool có thể được xây dựng với mức độ song song mục tiêu nhất định; theo mặc định, bằng với số lượng bộ xử lý có sẵn.

Điều này cũng có nghĩa là nếu bạn có các luồng song song lồng nhau hoặc nhiều luồng song song bắt đầu đồng thời, tất cả chúng sẽ chia sẻ cùng một nhóm. Ưu điểm: bạn sẽ không bao giờ sử dụng nhiều hơn mặc định (số lượng bộ xử lý có sẵn). Nhược điểm: bạn có thể không nhận được "tất cả bộ xử lý" được gán cho từng luồng song song mà bạn khởi tạo (nếu bạn có nhiều hơn một). (Rõ ràng bạn có thể sử dụng ManagedBlocker để phá vỡ điều đó.)

Để thay đổi cách thực hiện các luồng song song, bạn có thể

  • gửi thực thi luồng song song tới ForkJoinPool của riêng bạn: yourFJP.submit(() -> stream.parallel().forEach(soSomething)).get();hoặc
  • bạn có thể thay đổi kích thước của nhóm chung bằng cách sử dụng các thuộc tính hệ thống: System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20")cho mục tiêu song song của 20 luồng. Tuy nhiên, điều này không còn hoạt động sau bản vá backported https://bugs.openjdk.java.net/browse/JDK-8190974 .

Ví dụ về cái sau trên máy của tôi có 8 bộ xử lý. Nếu tôi chạy chương trình sau:

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

Đầu ra là:

215 216 216 216 216 216 216 216 316 316 316 316 316 316 316 316

Vì vậy, bạn có thể thấy rằng luồng song song xử lý 8 mục cùng một lúc, tức là nó sử dụng 8 luồng. Tuy nhiên, nếu tôi bỏ ghi chú dòng nhận xét, đầu ra là:

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

Lần này, luồng song song đã sử dụng 20 luồng và tất cả 20 phần tử trong luồng đã được xử lý đồng thời.


30
Cái commonPoolthực sự có ít hơn một availableProcessors, dẫn đến tổng số song song bằng availableProcessorsvì luồng gọi được tính là một.
Marko Topolnik

2
nộp lại ForkJoinTask. Để bắt chước parallel() get()là cần thiết:stream.parallel().forEach(soSomething)).get();
Grigory Kislin

5
Tôi không tin rằng ForkJoinPool.submit(() -> stream.forEach(...))sẽ chạy các hành động Stream của tôi với cái đã cho ForkJoinPool. Tôi hy vọng rằng toàn bộ Hành động Stream được thực thi trong ForJoinPool dưới dạng MỘT hành động, nhưng bên trong vẫn sử dụng ForkJoinPool mặc định / chung. Bạn đã thấy ở đâu, rằng ForkJoinPool.submit () sẽ làm những gì bạn nói?
Frederic Leitenberger

@FredericLeitenberger Có lẽ bạn muốn đặt bình luận của bạn bên dưới câu trả lời của Lukas.
assylias

2
Tôi thấy bây giờ stackoverflow.com/a/34930831/1520422 cho thấy độc đáo rằng nó thực sự hoạt động như đã thông báo. Nhưng tôi vẫn không hiểu làm thế nào nó hoạt động. Nhưng tôi ổn với "nó hoạt động". Cảm ơn!
Frederic Leitenberger

39

Ngoài ra, để kích hoạt tính toán song song bên trong forkJoinPool của riêng bạn, bạn cũng có thể chuyển nhóm đó sang phương thức CompleteableFuture.supplyAsync như trong:

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

22

Giải pháp ban đầu (thiết lập thuộc tính song song chung ForkJoinPool) không còn hoạt động. Nhìn vào các liên kết trong câu trả lời ban đầu, một bản cập nhật phá vỡ điều này đã được chuyển trở lại Java 8. Như đã đề cập trong các luồng được liên kết, giải pháp này không được đảm bảo để hoạt động mãi mãi. Dựa vào đó, giải pháp là forkjoinpool.submit với giải pháp .get được thảo luận trong câu trả lời được chấp nhận. Tôi nghĩ rằng backport cũng sửa chữa sự không đáng tin cậy của giải pháp này.

ForkJoinPool fjpool = new ForkJoinPool(10);
System.out.println("stream.parallel");
IntStream range = IntStream.range(0, 20);
fjpool.submit(() -> range.parallel()
        .forEach((int theInt) ->
        {
            try { Thread.sleep(100); } catch (Exception ignore) {}
            System.out.println(Thread.currentThread().getName() + " -- " + theInt);
        })).get();
System.out.println("list.parallelStream");
int [] array = IntStream.range(0, 20).toArray();
List<Integer> list = new ArrayList<>();
for (int theInt: array)
{
    list.add(theInt);
}
fjpool.submit(() -> list.parallelStream()
        .forEach((theInt) ->
        {
            try { Thread.sleep(100); } catch (Exception ignore) {}
            System.out.println(Thread.currentThread().getName() + " -- " + theInt);
        })).get();

Tôi không thấy sự thay đổi song song khi tôi thực hiện ForkJoinPool.commonPool().getParallelism()ở chế độ gỡ lỗi.
d-coder

Cảm ơn. Tôi đã làm một số thử nghiệm / nghiên cứu và cập nhật câu trả lời. Có vẻ như một bản cập nhật đã thay đổi nó, vì nó hoạt động trong các phiên bản cũ hơn.
Tod Casasent

Tại sao tôi tiếp tục nhận được điều này: unreported exception InterruptedException; must be caught or declared to be thrownngay cả với tất cả các catchngoại lệ trong vòng lặp.
Rocky Li

Rocky, tôi không thấy bất kỳ lỗi nào. Biết phiên bản Java và dòng chính xác sẽ giúp. "Interrupttedception" cho thấy việc thử / bắt xung quanh giấc ngủ không được đóng đúng trong phiên bản của bạn.
Tod Casasent

13

Chúng ta có thể thay đổi song song mặc định bằng cách sử dụng thuộc tính sau:

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

có thể thiết lập để sử dụng song song hơn.


Mặc dù đó là một thiết lập toàn cầu, nhưng nó hoạt động để tăng song
songStream

Điều này làm việc cho tôi trên phiên bản openjdk "1.8.0_222"
abbas

Cùng một người như trên, điều này không hiệu quả với tôi trên openjdk "11.0.6"
abbas

8

Để đo số lượng thực tế của các chủ đề được sử dụng, bạn có thể kiểm tra Thread.activeCount():

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

Điều này có thể tạo ra trên CPU 4 lõi một đầu ra như:

5 // common pool
23 // custom pool

Không có .parallel()nó cho:

3 // common pool
4 // custom pool

6
Thread.activeCount () không cho bạn biết chủ đề nào đang xử lý luồng của bạn. Thay vào đó, ánh xạ tới Thread.cienThread (). GetName (), theo sau là một dấu phân biệt (). Sau đó, bạn sẽ nhận ra rằng không phải mọi luồng trong nhóm sẽ được sử dụng ... Thêm một độ trễ cho quá trình xử lý của bạn và tất cả các luồng trong nhóm sẽ được sử dụng.
keyoxy

7

Cho đến bây giờ, tôi đã sử dụng các giải pháp được mô tả trong câu trả lời của câu hỏi này. Bây giờ, tôi đã nghĩ ra một thư viện nhỏ có tên Parallel Stream Support cho điều đó:

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

Nhưng như @PabloMatiasGomez đã chỉ ra trong các bình luận, có những nhược điểm liên quan đến cơ chế phân tách của các luồng song song phụ thuộc nhiều vào kích thước của nhóm chung. Xem luồng song song từ Hashset không chạy song song .

Tôi chỉ sử dụng giải pháp này để có các nhóm riêng biệt cho các loại công việc khác nhau nhưng tôi không thể đặt kích thước của nhóm chung thành 1 ngay cả khi tôi không sử dụng nó.


4

Ghi chú: Dường như có một bản sửa lỗi được triển khai trong JDK 10 để đảm bảo Nhóm luồng tùy chỉnh sử dụng số lượng luồng dự kiến.

Việc thực hiện luồng song song trong một ForkJoinPool tùy chỉnh phải tuân theo sự song song https://bugs.openjdk.java.net/browse/JDK-8190974


1

Tôi đã thử ForkJoinPool tùy chỉnh như sau để điều chỉnh kích thước nhóm:

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

Đây là đầu ra cho biết pool đang sử dụng nhiều luồng hơn 4 mặc định .

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]

Nhưng thực sự có một điều kỳ lạ , khi tôi cố gắng đạt được kết quả tương tự bằng cách sử dụng ThreadPoolExecutornhư sau:

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

nhưng tôi đã thất bại.

Nó sẽ chỉ bắt đầu song songStream trong một luồng mới và sau đó mọi thứ khác đều giống nhau, điều này một lần nữa chứng minh rằng parallelStreamsẽ sử dụng ForkJoinPool để bắt đầu các luồng con của nó.


Điều gì có thể là lý do có thể đằng sau việc không cho phép các giám đốc điều hành khác?
omjego

@omjego Đó là một câu hỏi hay có lẽ bạn có thể bắt đầu một câu hỏi mới và cung cấp thêm chi tiết để xây dựng ý tưởng của mình;)
Nghe

1

Đi để có được AbacusUtil . Số luồng có thể được chỉ định cho luồng song song. Đây là mã mẫu:

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

Tiết lộ tiết lộ Tôi là nhà phát triển của AbacusUtil.


1

Nếu bạn không muốn dựa vào các bản hack thực hiện, luôn có cách để đạt được điều tương tự bằng cách triển khai các bộ sưu tập tùy chỉnh sẽ kết hợp mapcollectngữ nghĩa ... và bạn sẽ không bị giới hạn trong ForkJoinPool:

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

May mắn thay, nó đã được thực hiện ở đây và có sẵn trên Maven Central: http://github.com/pivovarit/abul-collector

Tuyên bố miễn trừ trách nhiệm: Tôi đã viết nó và chịu trách nhiệm về nó.


0

Nếu bạn không phiền khi sử dụng thư viện của bên thứ ba, với phản ứng cyclops, bạn có thể trộn các luồng liên tục và song song trong cùng một đường ống và cung cấp ForkJoinPools tùy chỉnh. Ví dụ

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

Hoặc nếu chúng tôi muốn tiếp tục xử lý trong Luồng tuần tự

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

[Tiết lộ Tôi là nhà phát triển chính của cyclops-Reac]


0

Nếu bạn không cần một ThreadPool tùy chỉnh nhưng bạn muốn giới hạn số lượng tác vụ đồng thời, bạn có thể sử dụng:

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

(Câu hỏi trùng lặp yêu cầu này đã bị khóa, vì vậy xin vui lòng cho tôi ở đây)


-2

bạn có thể thử triển khai ForkJoinWorkerThreadFactory này và đưa nó vào lớp Fork-Join.

public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode) {
        this(checkParallelism(parallelism),
             checkFactory(factory),
             handler,
             asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
             "ForkJoinPool-" + nextPoolId() + "-worker-");
        checkPermission();
    }

bạn có thể sử dụng hàm tạo này của nhóm Fork-Join để thực hiện việc này.

lưu ý: - 1. nếu bạn sử dụng điều này, hãy xem xét dựa trên việc bạn thực hiện các luồng mới, việc lập lịch trình từ JVM sẽ bị ảnh hưởng, thường lên lịch các luồng kết nối đến các lõi khác nhau (được coi là một luồng tính toán). 2. lập lịch tác vụ bằng cách tham gia rẽ nhánh đến các luồng sẽ không bị ảnh hưởng. 3. Không thực sự tìm ra cách luồng song song chọn luồng từ liên kết ngã ba (không thể tìm thấy tài liệu phù hợp về nó), vì vậy hãy thử sử dụng một nhà máy luồng khác để đảm bảo, nếu luồng được chọn song song từ customThreadFactory mà bạn cung cấp. 4. commonThreadPool sẽ không sử dụng customThreadFactory này.


Bạn có thể cung cấp một ví dụ có thể sử dụng để giải thích cách sử dụng những gì bạn đã chỉ định không?
J. Murray
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.