据我所知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
答案 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 Peine和elusive-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