ForkJoinPool在invokeAll / join期间停止

我尝试使用ForkJoinPool来并行化我的CPU密集型计算。 我对ForkJoinPool的理解是,只要任何任务可以执行,它就会继续工作。 不幸的是,我经常观察到工作线程空闲/等待,因此并非所有CPU都保持忙碌状态。 有时我甚至观察到额外的工作线程。

我没想到这一点,因为我严格尝试使用非阻塞任务。 我的观察非常类似于ForkJoinPool似乎浪费了一个线程 。 在调试了很多ForkJoinPool之后,我有一个猜测:

我使用invokeAll()在子任务列表上分配工作。 在invokeAll()完成后执行第一个任务本身,它开始加入其他任务。 这样可以正常工作,直到下一个要连接的任务位于执行队列之上。 不幸的是,我提交了异步的其他任务而没有加入它们。 我期望ForkJoin框架首先继续执行这些任务,然后再转回加入任何剩余的任务。

但它似乎不是这样工作的。 相反,工作线程停止调用wait()直到等待的任务准备好(可能由其他工作线程执行)。 我没有validation这一点,但似乎是调用join()的一般缺陷。

ForkJoinPool提供了一个asyncMode ,但这是一个全局参数,不能用于单独的提交。 但我喜欢看到我的异步分叉任务很快就会执行。

那么,为什么ForkJoinTask.doJoin()不是简单地在其队列之上执行任何可用任务,直到它准备好(由自己执行或被其他人窃取)?

你对join()是对的。 两年前我写这篇文章指出了join()的问题。

正如我在那里所说,框架不能执行新提交的请求,直到完成之前的请求。 并且每个WorkThread都无法窃取,直到当前请求结束,这导致wait()。

您看到的其他线程是“延续线程”。 由于join()最终会发出wait(),因此需要这些线程,因此整个框架不会停止。

由于没有其他人似乎理解我的问题,我试着解释一下我调试后发现的情况:

如果所有fork / join调用都是严格配对的话,ForkJoinTasks的当前实现很有效。 通过一个开放括号来说明一个fork并通过一个闭合的连接,一个完美的二进制fork连接模式可能如下所示:

{([] [])([] [])} {([] [])([] [])}

如果你使用invokeAll(),你也可以提交如下的子任务列表:

{([] [] [] [])([] [] [] [])([] [] [] [])}

我所做的却看起来像这样的模式:

{([)([)} …]]

您可能会认为这看起来很不好或者是对fork-join框架的误用。 但唯一的限制是, 任务完成依赖是非循环的 ,否则你可能遇到死锁。 只要我的[]任务不依赖于()任务,我就不会发现它有任何问题。 违规]]只是表示我不会明确地等待它们; 他们有一天可能会完成,对我来说并不重要(在那一点上)。

实际上,当前的实现能够执行我的互锁任务,但只能通过产生额外的辅助线程,这是非常低效的。

缺陷似乎是join()的当前实现:join a 期望看到它的对应在它的执行队列之上,但是它找到了[并且很困惑。而不是简单地执行[]来摆脱它,当前线程挂起(调用wait()),直到其他人来执行意外任务。这导致性能急剧下降。

我的主要目的是将额外的工作放在队列上,以防止工作线程在队列空运行时挂起。 不幸的是相反的情况发生:-(

您没有将此框架用于其预期的非常狭窄的目的。

该框架始于2000年研究论文中的实验。 从那时起它就被修改了,但基本设计,大型数组上的fork-and-join仍然是相同的。 基本目的是教本科生如何沿着平衡树的树叶走下去。 当人们使用它而不是简单的数组处理时,会发生奇怪的事情。 它在Java7中的作用超出了我的意义; 这是本文的目的。

问题只会在Java8中变得更糟。 在那里它是驱动所有流并行工作的引擎。 阅读该文章的第二部分。 lambda兴趣列表中包含线程停顿,堆栈溢出和内存不足错误的报告。

如果不将其用于大型数据结构的纯递归分解,则使用它需要您自担风险。 即使这样,它创建的过multithreading也会造成严重破坏。 我不打算进一步讨论这个问题。