7

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

我没想到会这样,因为我严格尝试使用非阻塞任务。我的观察与 ForkJoinPool 的观察非常相似,似乎浪费了一个线程。在对 ForkJoinPool 进行大量调试后,我有一个猜测:

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

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

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

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

4

3 回答 3

3

由于似乎没有其他人理解我的问题,因此我尝试解释经过几个晚上的调试后发现的内容:

如果所有 fork/join 调用都严格配对,则 ForkJoinTasks 的当前实现运行良好。用一个左括号来说明一个叉子,用一个右括号来连接,一个完美的二叉叉连接模式可能如下所示:

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

如果你使用 invokeAll() 你也可以像这样提交子任务列表:

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

然而,我所做的看起来像这种模式:

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

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

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

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

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

于 2013-06-06T17:28:03.470 回答
3

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

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

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

于 2013-06-03T13:51:37.690 回答
2

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

该框架开始于 2000 年研究论文中的实验。从那时起它已经被修改,但基本设计,大型阵列上的 fork-and-join 保持不变。基本目的是教本科生如何走下平衡树的叶子。当人们将它用于简单的数组处理之外时,就会发生奇怪的事情。它在 Java7 中所做的事情超出了我的范围。这就是这篇文章的目的。

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

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

于 2013-06-07T13:48:06.390 回答