What is ForkJoinPool Async mode
Asked Answered
T

2

25

What does Async mode of ForkJoinPool mean? Javadoc mentions that it makes queues (is it per-thread queue?) FIFO instead of LIFO. What does it mean in practice?

Trenchant answered 12/4, 2011 at 18:42 Comment(0)
O
24

Each worker thread in a ForkJoinPool has its own work queue. Async mode concerns the order in which each worker takes forked tasks that are never joined from its work queue.

Workers in a ForkJoinPool in async mode process such tasks in FIFO (first in, first out) order. By default, ForkJoinPools process such tasks in LIFO (last in, first out) order.

It's important to emphasise that the async mode setting only concerns forked tasks that are never joined. When using a ForkJoinPool for what it was originally designed for, namely recursive fork/join task decomposition, asyncMode doesn't come into play at all. Only when a worker is not engaged in actual fork/join processing does it execute async tasks, and only then is the asyncMode flag actually queried.

Here's a small program that demonstrates the difference between the two different async mode settings:

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Demo of {@code ForkJoinPool} behaviour in async and non-async mode.
 */
public class ForkJoinAsyncMode {
    public static void main(String[] args) {
        // Set the asyncMode argument below to true or false as desired:
        ForkJoinPool pool = new ForkJoinPool(
                4, ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);

        pool.invoke(new RecursiveRangeAction(0, 200));
        pool.awaitQuiescence(2L, TimeUnit.SECONDS);
    }

    /**
     * A {@code ForkJoinTask} that prints a range if the range is smaller than a
     * certain threshold; otherwise halves the range and proceeds recursively.
     * Every recursive invocation also forks off a task that is never joined.
     */
    private static class RecursiveRangeAction extends RecursiveAction {
        private static final AtomicInteger ASYNC_TASK_ID = new AtomicInteger();

        private final int start;
        private final int end;

        RecursiveRangeAction(int start, int end) {
            this.start = start;
            this.end = end;
        }

        @Override
        protected void compute() {
            if (end - start < 10) {
                System.out.format("%s range [%d-%d] done%n",
                        Thread.currentThread().getName(), start, end);
            } else {
                int mid = (start + end) >>> 1;
                int id = ASYNC_TASK_ID.incrementAndGet();

                System.out.format(
                        "%1$s [%2$d-%3$d] -< [%3$d-%4$d], fork async task %5$d%n",
                        Thread.currentThread().getName(), start, mid, end, id);

                // Fork off additional asynchronous task that is never joined.
                ForkJoinTask.adapt(() -> {
                    System.out.format("%s async task %d done%n",
                            Thread.currentThread().getName(), id);
                }).fork();

                invokeAll(new RecursiveRangeAction(start, mid),
                        new RecursiveRangeAction(mid, end));
            }
        }
    }
}

In non-async mode (the default for ForkJoinPool), forked tasks that are never joined are executed in LIFO order.

When you run the example program in non-async mode, looking at the output of one worker you might see a pattern like the following:

ForkJoinPool-1-worker-0 [175-187] -< [187-200], fork async task 10
ForkJoinPool-1-worker-0 [175-181] -< [181-187], fork async task 11
ForkJoinPool-1-worker-0 range [175-181] done
ForkJoinPool-1-worker-0 range [181-187] done
ForkJoinPool-1-worker-0 [187-193] -< [193-200], fork async task 12
ForkJoinPool-1-worker-0 range [187-193] done
ForkJoinPool-1-worker-0 range [193-200] done
ForkJoinPool-1-worker-0 async task 12 done
ForkJoinPool-1-worker-0 async task 11 done
ForkJoinPool-1-worker-0 async task 10 done

Here, tasks 10, 11, 12 are forked and later executed in reverse order once the worker gets around to executing them.

In async mode on the other hand, again looking at the output of one worker the pattern would rather look like the following:

ForkJoinPool-1-worker-3 [150-175] -< [175-200], fork async task 8
ForkJoinPool-1-worker-3 [150-162] -< [162-175], fork async task 9
ForkJoinPool-1-worker-3 [150-156] -< [156-162], fork async task 10
ForkJoinPool-1-worker-3 range [150-156] done
ForkJoinPool-1-worker-3 range [156-162] done
ForkJoinPool-1-worker-3 [162-168] -< [168-175], fork async task 11
...
ForkJoinPool-1-worker-3 async task 8 done
ForkJoinPool-1-worker-3 async task 9 done
ForkJoinPool-1-worker-3 async task 10 done
ForkJoinPool-1-worker-3 async task 11 done

Tasks 8, 9, 10, 11 are forked and later executed in the order they were submitted.

When to use which mode? Whenever a ForkJoinPool thread pool is chosen to take advantage of its work-stealing properties rather than for recursive fork/join task processing, async mode is probably the more natural choice, as tasks get executed in the order they are submitted.

Async event-driven frameworks like CompletableFuture are sometimes said to profit from async mode. For example, when constructing a complex chain of CompletableFuture callbacks, then a custom ForkJoinPool executor in async mode might perform slightly better than the default executor. (I can't speak from experience though.)

Ossify answered 9/1, 2016 at 23:38 Comment(2)
What's the purpose though? Latency? Making it more CPU-friendly somehow?Penland
In order to utilize the asyncMode, I would recommend using Executors.newWorkStealingPool(). Your custom ForkJoinPool implementation is same as the work-stealing ExecutorService, with just a change in the parallelism count. My General Rule of Thumb for LIFO: ForkJoinPool's commonPool, for FIFO: work-stealing ExecutorService.Butene
S
7

It is meant for event-style tasks that are submitted but never joined. So basically tasks that are getting executed for their side-effects, not for returning a result that will be processed by the forking task after joining.

Sessoms answered 31/1, 2012 at 17:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.