我尝试使用ForkJoinPool来并行化我的CPU密集型计算。我对ForkJoinPool的理解是,只要有任务可以执行,它就会继续工作。不幸的是,我经常观察到工作线程空闲/等待,因此并非所有CPU都保持繁忙。有时我甚至观察到额外的工作线程。
我没有预料到这一点,因为我严格尝试使用非阻塞任务。我的观察与ForkJoinPool非常相似,似乎浪费了一个线程。在对ForkJoinPool进行了大量调试后,我有一个猜测:
我使用invokeAll()将工作分配到一个子任务列表上。在invokeAll()完成第一个任务本身的执行后,它开始加入其他任务。这可以正常工作,直到下一个要加入的任务位于执行队列的顶部。不幸的是,我异步地提交了额外的任务,而没有加入它们。我希望ForkJoin框架先继续执行这些任务,然后再返回加入剩余的任务。
但似乎不是这样的。相反,工作线程调用wait()时会停滞,直到等待的任务准备好(可能由另一个工作线程执行)。我没有验证这一点,但这似乎是调用join()的一个普遍缺陷。
ForkJoinPool提供了一个asyncMode,但这是一个全局参数,不能用于单独的提交。但是我希望我的异步分叉任务能很快被执行。
那么,为什么ForkJoinTask.doJoin()不简单地在它的队列上执行任何可用的任务,直到它准备好(要么由自己执行,要么被其他人窃取)?
既然没有人理解我的问题,我试着解释一下经过几个晚上的调试后我的发现:
如果所有的fork/join调用都是严格配对的,那么ForkJoinTasks的当前实现可以很好地工作。用左括号表示一个fork,用闭括号表示一个join,一个完美的二进制fork join模式可能是这样的:
{([][]) ([][])} {([][]) ([][])}
如果你使用invokeAll(),你也可以像这样提交子任务列表:
{([][][][]) ([][][][]) ([][][][])}
我所做的看起来像这样:
{([)([)}…]]
你可能会认为这看起来很糟糕,或者是对fork-join框架的误用。但唯一的约束是,任务完成依赖项是无循环的,否则您可能会遇到死锁。只要我的[]任务不依赖于()任务,我不认为它有任何问题。令人反感的]]只是明确地表示我不等待它们;他们总有一天会完成的,这对我来说无关紧要。
确实,当前的实现能够执行我的联锁任务,但只能通过产生额外的辅助线程,这是相当低效的。
缺陷似乎是join()的当前实现:连接一个)期望在其执行队列顶部看到其对应的(),但是它发现了一个[]并且感到困惑。当前线程不是简单地执行[]来摆脱它,而是挂起(调用wait()),直到其他人来执行意外的任务。这将导致严重的性能崩溃。我的主要意图是在队列上添加额外的工作,以防止在队列为空时工作线程挂起。不幸的是,相反的情况发生了:-(
关于join()你是完全正确的。我在两年前写了这篇文章,指出了join()的问题。
如我所说,框架在完成之前提交的请求之前不能执行新提交的请求。并且每个工作线程在当前请求完成之前不能窃取,这会导致wait()。
您看到的额外线程是"延续线程"。因为join()最终会发出wait(),所以需要这些线程来保证整个框架不会停滞。
您没有将此框架用于它所预期的非常狭窄的目的。
该框架始于2000年研究论文中的实验。从那以后,它被修改了,但是基本的设计,在大数组上的fork- join,仍然是一样的。基本目的是教本科生如何在一棵平衡的树上行走。当人们将它用于简单的数组处理之外的事情时,就会发生奇怪的事情。我不知道它在Java7中做了什么;这就是这篇文章的目的。
这些问题在Java8中只会变得更糟。这就是驱动所有流并行工作的引擎。读一读那篇文章的第二部分。lambda兴趣列表中充满了线程停滞、堆栈溢出和内存不足错误的报告。
当您不将其用于大型数据结构的纯递归分解时,您将自担风险。即使这样,它创建的过多线程也会造成严重破坏。我不打算继续讨论这个问题了