是什么决定了Java ForkJoinPool创建的线程数?

时间:2012-05-29 10:44:22

标签: java parallel-processing threadpool fork-join

据我所知ForkJoinPool,该池创建固定数量的线程(默认值:核心数)并且永远不会创建更多线程(除非应用程序使用{{1表示需要这些线程) }})。

然而,使用managedBlock我发现在一个创建30,000个任务(ForkJoinPool.getPoolSize())的程序中,执行这些任务的RecursiveAction平均使用700个线程(每次执行任务时计算的线程数)被建造)。任务不做I / O,而是纯粹的计算;唯一的任务间同步是调用ForkJoinPool并访问ForkJoinTask.join(),即没有线程阻塞操作。

由于AtomicBoolean没有像我理解的那样阻塞调用线程,所以没有理由为什么池中的任何线程都应该阻塞,所以(我假设)没有理由再创建线程(显然正在发生)。

那么,为什么join()创建了这么多线程呢?哪些因素决定了创建的线程数?

我原本希望这个问题可以在不发布代码的情况下得到解答,但这里可以根据要求提出。此代码摘自四倍大小的程序,简化为必要部分;它不会按原样编译。如果需要,我当然也可以发布完整的程序。

程序使用深度优先搜索在迷宫中搜索从给定起点到给定终点的路径。保证存在解决方案。主要逻辑在ForkJoinPool的{​​{1}}方法中:A compute()从某个给定点开始,并继续从当前点可到达的所有邻居点。它不是在每个分支点创建一个新的SolverTask(这将创建太多的任务),而是将除了一个之外的所有邻居推送到后退堆栈以便稍后处理,并继续只有一个邻居没有被推送到堆栈。一旦它以这种方式达到死胡同,就会弹出最近推到回溯堆栈的点,并从那里继续搜索(相应地减少从taks起点构建的路径)。一旦任务发现其回溯堆栈大于某个阈值,就会创建一个新任务;从那时起,任务在继续从其回溯堆栈中弹出直到耗尽时,在到达分支点时不会将任何其他点推送到其堆栈,而是为每个这样的点创建新任务。因此,可以使用堆栈限制阈值来调整任务的大小。

我上面引用的数字(“30,000个任务,平均700个线程”)来自于搜索5000x5000个单元格的迷宫。所以,这是基本代码:

RecursiveAction

5 个答案:

答案 0 :(得分:16)

有关stackoverflow的相关问题:

ForkJoinPool stalls during invokeAll/join

ForkJoinPool seems to waste a thread

我制作了一个可运行的精简版本(正在使用的jvm参数:-Xms256m -Xmx1024m -Xss8m):

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.TimeUnit;

public class Test1 {

    private static ForkJoinPool pool = new ForkJoinPool(2);

    private static class SomeAction extends RecursiveAction {

        private int counter;         //recursive counter
        private int childrenCount=80;//amount of children to spawn
        private int idx;             // just for displaying

        private SomeAction(int counter, int idx) {
            this.counter = counter;
            this.idx = idx;
        }

        @Override
        protected void compute() {

            System.out.println(
                "counter=" + counter + "." + idx +
                " activeThreads=" + pool.getActiveThreadCount() +
                " runningThreads=" + pool.getRunningThreadCount() +
                " poolSize=" + pool.getPoolSize() +
                " queuedTasks=" + pool.getQueuedTaskCount() +
                " queuedSubmissions=" + pool.getQueuedSubmissionCount() +
                " parallelism=" + pool.getParallelism() +
                " stealCount=" + pool.getStealCount());
            if (counter <= 0) return;

            List<SomeAction> list = new ArrayList<>(childrenCount);
            for (int i=0;i<childrenCount;i++){
                SomeAction next = new SomeAction(counter-1,i);
                list.add(next);
                next.fork();
            }


            for (SomeAction action:list){
                action.join();
            }
        }
    }

    public static void main(String[] args) throws Exception{
        pool.invoke(new SomeAction(2,0));
    }
}

显然,当您执行连接时,当前线程会看到所需的任务尚未完成,并为自己执行另一项任务。

它发生在java.util.concurrent.ForkJoinWorkerThread#joinTask

但是,这个新任务会产生更多相同的任务,但是它们无法在池中找到线程,因为线程在连接中被锁定。而且由于它无法知道释放它们需要多长时间(线程可能处于无限循环或永久死锁),因此会产生新的线程(补偿连接的线程为 Louis Wasserman 提到):java.util.concurrent.ForkJoinPool#signalWork

因此,为了防止这种情况,您需要避免递归产生任务。

例如,如果在上面的代码中将初始参数设置为1,则活动线程数量将为2,即使您将childrenCount增加十倍。

另请注意,虽然活动线程数量增加,但正在运行的线程数量小于或等于 parallelism

答案 1 :(得分:10)

来自源评论:

  

补偿:除非已有足够的活动线程,否则方法tryPreBlock()可能会创建或重新激活备用线程,以补偿阻塞的加入者,直到它们解除阻塞。

我认为发生的事情是你没有很快完成任何任务,而且由于在你提交新任务时没有可用的工作线程,所以会创建一个新线程。

答案 2 :(得分:8)

strict,full-strict和terminally-strict与处理有向无环图(DAG)有关。您可以谷歌这些条款,以充分了解它们。这是框架旨在处理的处理类型。查看API for Recursive ...中的代码,框架依赖于您的compute()代码来执行其他compute()链接,然后执行join()。每个任务都像处理DAG一样执行单个连接()。

您没有进行DAG处理。您正在分配许多新任务并在每个任务上等待(join())。阅读源代码。它非常复杂,但你可能能够弄明白。该框架没有做适当的任务管理。当它执行join()时,它将把等待的任务放在哪里?没有挂起的队列,需要监视器线程不断查看队列以查看完成的内容。这就是框架使用&#34;延续线程&#34;的原因。当一个任务确实加入()时,框架假设它正在等待单个较低的任务完成。当存在许多join()方法时,线程无法继续,因此需要存在辅助或继续线程。

如上所述,您需要一个分散 - 聚集类型的fork-join过程。在那里你可以分叉任务数

答案 3 :(得分:2)

Holger Peineelusive-code发布的两个代码段实际上并未遵循javadoc for 1.8 version中出现的推荐做法:

  

在最典型的用法中,fork-join对就像一个调用   (fork)并从并行递归函数返回(join)。原样   其他形式的递归调用,返回(连接)的情况   应该在最里面进行。例如, a.fork();   b.fork(); b.join(); a.join(); 可能会更多   比在代码 b 之前加入代码 a 更有效。

在这两种情况下,FJPool都是通过默认构造函数实例化的。这导致使用 asyncMode = false 构建池,这是默认值:

  

@param asyncMode如果为true,
      为forked建立本地先进先出调度模式      从未加入的任务。这种模式可能更合适      比应用程序中的默认本地基于堆栈的模式      工作线程只处理事件式异步任务。      对于默认值,请使用false。

那种工作队列实际上是lifo:
头 - &gt; | t4 | t3 | t2 | t1 | ...... | &lt; - tail

因此,在片段中,他们 fork()所有任务都将它们推送到堆栈而不是 join()以相同的顺序,即从最深的任务(t1)到最顶层(t4)有效阻塞,直到某个其他线程将窃取(t1),然后(t2)等等。 由于存在阻止所有池线程的任务(task_count&gt;&gt;&gt; pool.getParallelism()),因此所述补偿开始为Louis Wasserman

答案 4 :(得分:0)

值得注意的是,elusive-code发布的代码的输出取决于Java的版本。 在Java 8中运行代码,我看到输出:

...
counter=0.73 activeThreads=45 runningThreads=5 poolSize=49 queuedTasks=105 queuedSubmissions=0 parallelism=2 stealCount=3056
counter=0.75 activeThreads=46 runningThreads=1 poolSize=51 queuedTasks=0 queuedSubmissions=0 parallelism=2 stealCount=3158
counter=0.77 activeThreads=47 runningThreads=3 poolSize=51 queuedTasks=0 queuedSubmissions=0 parallelism=2 stealCount=3157
counter=0.74 activeThreads=45 runningThreads=3 poolSize=51 queuedTasks=5 queuedSubmissions=0 parallelism=2 stealCount=3153

但是在Java 11中运行相同的代码,输出是不同的:

...
counter=0.75 activeThreads=1 runningThreads=1 poolSize=2 queuedTasks=4 queuedSubmissions=0 parallelism=2 stealCount=0
counter=0.76 activeThreads=1 runningThreads=1 poolSize=2 queuedTasks=3 queuedSubmissions=0 parallelism=2 stealCount=0
counter=0.77 activeThreads=1 runningThreads=1 poolSize=2 queuedTasks=2 queuedSubmissions=0 parallelism=2 stealCount=0
counter=0.78 activeThreads=1 runningThreads=1 poolSize=2 queuedTasks=1 queuedSubmissions=0 parallelism=2 stealCount=0
counter=0.79 activeThreads=1 runningThreads=1 poolSize=2 queuedTasks=0 queuedSubmissions=0 parallelism=2 stealCount=0