27

我想Runnable通过一种方法将任务提交到 ForkJoinPool:

forkJoinPool.submit(Runnable task)

注意,我使用 JDK 7。

在后台,它们被转换为 ForkJoinTask 对象。我知道 ForkJoinPool 在将任务递归地拆分为较小的任务时是有效的。

问题:

如果没有递归,工作窃取在 ForkJoinPool 中是否仍然有效?

在这种情况下值得吗?

更新 1: 任务很小并且可能不平衡。即使对于严格相等的任务,诸如上下文切换、线程调度、停放、页面未命中等之类的事情也会阻碍导致不平衡

更新 2: Doug Lea 在并发 JSR-166 兴趣组中写道,给出了一个提示:

当所有任务都是异步的并提交到池而不是分叉时,这也大大提高了吞吐量,这成为构建参与者框架以及许多您可能使用 ThreadPoolExecutor 的普通服务的合理方式。

我认为,当涉及到相当小的 CPU 密集型任务时,ForkJoinPool 是要走的路,这要归功于这种优化。要点是这些任务已经很小,不需要递归分解。工作窃取工作,无论是大任务还是小任务 - 任务都可以被另一个空闲的工作人员从忙碌的工作人员的双端队列中抢走。

更新 3: ForkJoinPool 的可扩展性- Akka 乒乓球团队的基准测试显示了很好的结果。

尽管如此,要更有效地应用 ForkJoinPool 需要进行性能调整。

4

1 回答 1

18

ForkJoinPool源代码有一个很好的部分,称为“实现概述”,请仔细阅读以获得最终真相。下面的解释是我对JDK 8u40的理解。

从第一天开始,ForkJoinPool每个工作线程都有一个工作队列(我们称它们为“工作队列”)。分叉的任务被推入本地工作队列,准备再次被工作人员弹出并执行——换句话说,从工作线程的角度来看,它看起来像一个堆栈。当一个工作者耗尽它的工作者队列时,它会四处走动并试图从其他工作者队列中窃取任务。那就是“偷工减料”

现在,在 (IIRC) JDK 7u12 之前,ForkJoinPool只有一个全局提交队列。当工作线程用完本地任务以及要窃取的任务时,他们会到达那里并尝试查看是否有外部工作可用。在这种设计中,与常规的(例如,ThreadPoolExecutorArrayBlockingQueue.

之后发生了很大的变化。在此提交队列被确定为严重的性能瓶颈之后,Doug Lea 等人。也对提交队列进行了条带化​​。事后看来,这是一个显而易见的想法:您可以重用大多数可用于工作队列的机制。您甚至可以为每个工作线程松散地分配这些提交队列。现在,外部提交进入提交队列之一。然后,没有工作的工作人员可以先查看与特定工作人员相关的提交队列,然后四处寻找其他工作人员的提交队列。可以称之为“偷工减料”。

我已经看到许多工作负载从中受益。ForkJoinPool甚至对于普通的非递归任务,这种特殊的设计优势在很久以前就被认识到了。concurrency-interest@ 的许多用户要求一个简单的工作窃取执行器,而不需要所有的ForkJoinPool奥术。这就是为什么我们Executors.newWorkStealingPool()在 JDK 8 之后的原因之一——目前委托给ForkJoinPool,但为了提供更简单的实现而开放。

于 2015-05-06T18:54:21.080 回答