Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to configure a single-threaded ForkJoinPool?

Is it possible to configure ForkJoinPool to use 1 execution thread?

I am executing code that invokes Random inside a ForkJoinPool. Every time it runs, I end up with different runtime behavior, making it difficult to investigate regressions.

I would like the codebase to offer "debug" and "release" modes. "debug" mode would configure Random with a fixed seed, and ForkJoinPool with a single execution thread. "release" mode would use system-provided Random seeds and use the default number of ForkJoinPool threads.

I tried configuring ForkJoinPool with a parallelism of 1, but it uses 2 threads (main and a second worker thread). Any ideas?

like image 239
Gili Avatar asked Dec 01 '15 03:12

Gili


2 Answers

So, it turns out I was wrong.

When you configure a ForkJoinPool with parallelism set to 1, only one thread executes the tasks. The main thread is blocked on ForkJoin.get(). It doesn't actually execute any tasks.

That said, it turns out that it is really tricky providing deterministic behavior. Here are some of the problems I had to correct:

  • ForkJoinPool was executing tasks using different worker threads (with different names) if the worker thread became idle long enough. For example, if the main thread got suspended on a debugging breakpoint, the worker thread would become idle and shut down. When I would resume execution, ForkJoinThread would spin up a new worker thread with a different name. To solve this, I had to provide a custom ForkJoinWorkerThreadFactory implementation that returns null if the ForkJoinPool already has a live worker (this prevents the pool from creating multiple workers). I also had ensure that my code was returning the same Random instance even if a worker thread shut down and came back again.
  • Collections with non-deterministic iteration order such as HashMap or HashSet led to elements grabbing random numbers in a different order on every run. I corrected this by using LinkedHashMap and LinkedHashSet.
  • Objects with non-deterministic hashCode() implementations, such as Enum.hashCode(). I forget what problems this caused but I corrected it by calculating the hashCode() myself instead of relying on the built-in method.

Here is a sample implementation of ForkJoinWorkerThreadFactory:

class MyForkJoinWorkerThread extends ForkJoinWorkerThread
{
    MyForkJoinWorkerThread(ForkJoinPool pool)
    {
        super(pool);
        // Change thread name after ForkJoinPool.registerWorker() does the same
        setName("DETERMINISTIC_WORKER");
    }
}

ForkJoinWorkerThreadFactory factory = new ForkJoinWorkerThreadFactory()
{
    private WeakReference<Thread> currentWorker = new WeakReference<>(null);

    @Override
    public synchronized ForkJoinWorkerThread newThread(ForkJoinPool pool)
    {
        // If the pool already has a live thread, wait for it to shut down.
        Thread thread = currentWorker.get();
        if (thread != null && thread.isAlive())
        {
            try
            {
                thread.join();
            }
            catch (InterruptedException e)
            {
                log.error("", e);
            }
        }
        ForkJoinWorkerThread result = new MyForkJoinWorkerThread(pool);
        currentWorker = new WeakReference<>(result);
        return result;
    }
};
like image 124
Gili Avatar answered Sep 24 '22 08:09

Gili


Main thread is always the first thread your application will create. So when you create a ForkJoinPool with parallelism of 1, you are creating another thread. Effectively there will be two threads in the application now ( because you created a pool of threads ).

If you need only one thread that is Main, you can execute your code in sequence ( and not in parallel at all ).

like image 41
tuxdna Avatar answered Sep 23 '22 08:09

tuxdna