ForkJoinPool在invokeAll/join期间停滞

8
我尝试使用ForkJoinPool来并行计算我的CPU密集型任务。我理解ForkJoinPool会继续工作直到没有任务可执行。不幸的是,我经常观察到工作线程处于空闲/等待状态,因此并没有充分利用所有CPU。有时我甚至观察到额外的工作线程。
我没有预料到这一点,因为我严格尝试使用非阻塞式任务。我的观察结果与ForkJoinPool似乎浪费了一个线程非常相似。在对ForkJoinPool进行了大量调试后,我有一个猜想:
我使用invokeAll()将任务分配给子任务列表。在invokeAll()完成执行第一个任务后,它开始加入其他任务。这很好,直到要加入的下一个任务在执行队列的顶部。不幸的是,我异步提交了其他任务而没有加入它们。我希望ForkJoin框架能够继续先执行这些任务,然后再返回加入任何剩余的任务。
但是似乎这种方式不起作用。相反,工作线程会调用wait()而被阻塞,直到等待的任务准备就绪(可能是由另一个工作线程执行)。我没有验证过,但这似乎是调用join()的一个普遍缺陷。
ForkJoinPool提供了asyncMode,但这是一个全局参数,不能用于个别提交。但我希望看到我的异步forked任务能够很快执行。
那么,为什么ForkJoinTask.doJoin()不只是在其队列顶部执行任何可用任务,直到它准备就绪(由自己执行或被其他人窃取)呢?

你能和我们分享一些代码吗?有时候,三行代码胜过三十行散文。 - Fildor
我在这里放置了一些东西:http://pastebin.com/kgHuJZMM - Ditz
3个回答

4
由于似乎没有人理解我的问题,我尝试解释一下我在几个晚上调试后发现的情况:
如果所有的fork/join调用都是严格配对的,ForkJoinTasks的当前实现效果很好。通过使用开括号表示fork,闭括号表示join,一个完美的二进制fork join模式可能看起来像这样:
{([][]) ([][])} {([][]) ([][])}
如果您使用invokeAll(),您也可以提交子任务列表,如下所示:
{([][][][]) ([][][][]) ([][][][])}
然而,我做的事情看起来像这个模式:
{([) ([)} ... ]
你可以认为这看起来不好或者是滥用了fork-join框架。但唯一的限制是,任务完成的依赖关系是无环的,否则可能会遇到死锁。只要我的[]任务不依赖于()任务,我就看不出有什么问题。冒犯的]]只是表明我没有显式地等待它们;它们可能某天完成,对我来说并不重要(在那时)。
实际上,当前的实现能够执行我的交错任务,但是只能通过生成额外的辅助线程来完成,这相当低效。
缺陷似乎在于join()的当前实现:加入一个)期望在其执行队列的顶部看到其对应的(,但是它发现了一个[,感到困惑。与其简单地执行[]以摆脱它,当前线程挂起(调用wait()),直到有人来执行意外的任务。这导致了严重的性能下降。
我的初衷是将额外的工作放入队列中,以防止工作线程在队列为空时挂起。不幸的是,相反的情况发生了 :-(

3
你对join()的观点是正确的。两年前,我写了这篇文章,指出了join()的问题。这里
正如我所说,框架在完成早期请求之前无法执行新提交的请求。每个WorkThread在当前请求完成之前不能窃取,这会导致wait()。
你看到的额外线程是“续线程”。由于join()最终会发出wait(),因此需要这些线程,以免整个框架停顿不前。

是的,我读了你的文章,确实这让我得出了我的结论。我同意你的观点,即F/J框架无法处理被外部资源阻塞的任务。但我仍然不明白为什么WorkerThread不能执行任何其他可用的工作,无论是从任何尾部窃取还是从自己的顶部获取,直到等待的任务准备就绪。 - Ditz
责任。如果另一个请求的任务出现异常,则可能没有办法找到所有者。池只使用一个UncaughtExceptionHandler。您可以自己查看代码。这是一堆纷乱的代码,但您可能能够通过跟踪来解决问题。 - edharned
不太确定:如果执行从另一个队列中窃取的任务,则会出现相同的问题。当前的FJTask有责任捕获这些任务,并调用this.setExceptionalCompletion()而不是将它们传递给池的UncaughtExceptionHandler。问题在于:为什么不像执行从尾部窃取的任务一样,执行来自头部的待处理任务呢? - Ditz
2
这个框架是用于遍历平衡树叶的。你并没有做适合这个有限框架的事情。你得到的任何结果都没有现实基础。关于如何正确使用此框架,网络上有许多示例。 - edharned
这确实是我发现的问题:框架对我的使用方式感到困惑并停止了。请参见我的回答... - Ditz
显示剩余2条评论

2
你没有按照这个框架既定的非常狭窄的目的来使用它。
该框架最初是在2000年的研究论文中提出的实验。自那时以来已经进行了修改,但基本设计——针对大型数组的分叉和合并——保持不变。其基本目的是教授本科生如何沿着一个平衡树的叶子走。当人们将其用于简单数组处理之外的其他用途时,奇怪的事情会发生。在Java7中它正在做什么超出了我的理解范围;这也是文章的目的。
问题在Java8中只会变得更糟。在那里,它是驱动所有流并行工作的引擎。请在该文章的第二部分中阅读相关内容。Lambda感兴趣的列表中充满了线程停顿、堆栈溢出和内存不足错误的报告。
当你不将它用于大数据结构的纯递归分解时,使用它存在风险。即使在这种情况下,它创建的过多线程可能会造成混乱。我不会进一步讨论这个问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接