7

Related: CompletableFuture on ParallelStream gets batched and runs slower than sequential stream?

I'm doing some research on different ways of parallelizing network calls through parallelStream and CompletableFutures. As such, I have come across this situation where the ForkJoinPool.commonPool(), which is used by java's parallelStream, is dynamically growing in size, from ~ #Cores, to Max value of 64.

Java details: $ java -version

openjdk version "11.0.10" 2021-01-19
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.10+9)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.10+9, mixed mode)

Code that shows such behavior is below (Full executable code here)


    public static int loops = 100;
    private static long sleepTimeMs = 1000;
    private static ExecutorService customPool = Executors.newFixedThreadPool(loops);




    // this method shows dynamic increase in pool size
    public static void m1() {
        Instant start = Instant.now();
        LongSummaryStatistics stats = LongStream.range(0, loops).boxed()
                .parallel()
                .map(number -> CompletableFuture.supplyAsync(
                        () -> DummyProcess.slowNetworkCall(number), customPool))
                .map(CompletableFuture::join)
                .mapToLong(Long::longValue)
                .summaryStatistics();

    }

    // this method shows static pool size
    public static void m2() {
        Instant start = Instant.now();
        LongSummaryStatistics stats = LongStream.range(0, loops)
                .parallel()
                .map(DummyProcess::slowNetworkCall) // in this call, parallelism/poolsize stays constant 11
                .summaryStatistics();
    }


    public static Long slowNetworkCall(Long i) {
        Instant start = Instant.now();
        // starts with 11 (#cores in my laptop = 12), goes upto 64
        log.info(" {} going to sleep. poolsize: {}", i, ForkJoinPool.commonPool().getPoolSize());
        try {
            TimeUnit.MILLISECONDS.sleep(sleepTimeMs);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info(" {} woke up..", i);
        return Duration.between(start, Instant.now()).toMillis();
    }

Sample output:

16:07:17.443 [pool-2-thread-7] INFO  generalworks.parallelism.DummyProcess -  44 going to sleep. poolsize: 11
16:07:17.443 [pool-2-thread-9] INFO  generalworks.parallelism.DummyProcess -  7 going to sleep. poolsize: 12
16:07:17.443 [pool-2-thread-4] INFO  generalworks.parallelism.DummyProcess -  6 going to sleep. poolsize: 12
16:07:17.444 [pool-2-thread-13] INFO  generalworks.parallelism.DummyProcess -  82 going to sleep. poolsize: 13
16:07:17.444 [pool-2-thread-14] INFO  generalworks.parallelism.DummyProcess -  26 going to sleep. poolsize: 14
16:07:17.444 [pool-2-thread-15] INFO  generalworks.parallelism.DummyProcess -  96 going to sleep. poolsize: 15
16:07:17.445 [pool-2-thread-16] INFO  generalworks.parallelism.DummyProcess -  78 going to sleep. poolsize: 16
.
.
16:07:18.460 [pool-2-thread-79] INFO  generalworks.parallelism.DummyProcess -  2 going to sleep. poolsize: 64
16:07:18.460 [pool-2-thread-71] INFO  generalworks.parallelism.DummyProcess -  36 going to sleep. poolsize: 64
16:07:18.460 [pool-2-thread-74] INFO  generalworks.parallelism.DummyProcess -  77 going to sleep. poolsize: 64
16:07:18.461 [pool-2-thread-83] INFO  generalworks.parallelism.DummyProcess -  86 going to sleep. poolsize: 64

I understand that the number of Threads in a commonpool, i.e, it parallelism is based upon max number of available cores, so since my laptop has 12 cores, i get a parallelism of 11 to start with. But I do not understand why it keeps climbing in one method, but in the other one, it's size keeps constants

Somjit
  • 2,503
  • 5
  • 33
  • 60
  • It's not really obvious what the output corresponds to. You have different fragments of code but nothing we can actually run. Give us the complete test case. – Michael May 06 '21 at 11:26
  • @Michael here's the complete code: https://github.com/Suedo/AlgoDS/blob/f7e4fd367c7ecb4ca2d2ba9ff9cb266a46c78b89/src/main/java/generalworks/parallelism/DummyProcess.java – Somjit May 06 '21 at 11:29
  • what do you mean exactly with : "Especially when a a slight variation of the same...". You need to provide full, understandable example in your question itself. I for example, did not understand anything from that sentence. – Eugene May 06 '21 at 11:52
  • 2
    the part "But I do not understand why it keeps climbing" - I already addressed in the other questions comments. When you _block_ an inner threads that `ForkJoinPool` uses (and you _do_ that with `CompletableFuture::join`), more threads will be created to compensate, so that target parallelism stays correct. – Eugene May 06 '21 at 11:55
  • @Eugene ok, so in method1, due to join, its causing the poolsize to increase, but since no such join is in method 2, the poolsize stays same. I guess that solves it, didnt notice earlier. You can frame that in as an answer, and we can close this question as resolved – Somjit May 06 '21 at 12:22

2 Answers2

10

I believe your answer is here (ForkJoinPool implementation):

                        if ((wt = q.owner) != null &&
                            ((ts = wt.getState()) == Thread.State.BLOCKED ||
                             ts == Thread.State.WAITING))
                            ++bc;            // worker is blocking

In one version of your code, you block on Thread.sleep, which puts the thread into the TIMED_WAITING state, while in the other you block on CompletableFuture.join(), which puts it into the WAITING state. The implementation distinguishes between these and exhibits the different behaviors you have observed.

There is also special-cased code inside CompletableFuture that makes it cooperate with the ForkJoinPool in order to prevent starvation while waiting for the result:

            if (Thread.currentThread() instanceof ForkJoinWorkerThread)
                ForkJoinPool.helpAsyncBlocker(defaultExecutor(), q);

A conclusion relevant to the reason why you're testing this in the first place: Thread.sleep() does not properly simulate a long network call. If you did an actual one, or some other blocking operation, it would compensate by extending the pool.

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
3

The provided answer is absolutely correct. According to the documentation of ForJoinPool::getPoolSize:

The result returned by this method may differ from getParallelism() when threads are created to maintain parallelism when others are cooperatively blocked.

As you have seen, sleep does not count as cooperatively blocked (I guess it sort of makes sense). You can read a somehow related Q&A, here

Eugene
  • 117,005
  • 15
  • 201
  • 306