Java ForkJoinPool非递归任务,工作窃取是否有效?

28

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

forkJoinPool.submit(Runnable task)

注意,我使用的是JDK 7。

在底层,它们会被转换为ForkJoinTask对象。 我知道当任务递归地分成较小的任务时,ForkJoinPool非常高效。

问题:

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

在这种情况下是否值得使用?

更新1: 任务很小且可能不平衡。即使对于严格相等的任务,诸如上下文切换、线程调度、停车、页面错过等等也会导致不平衡

更新2: Doug Lea在Concurrency JSR-166 Interest组中写道,提供了此提示:

当所有任务都是异步的并提交到池中而不是分叉时,这也极大地提高了吞吐量,这成为构建演员框架以及许多纯服务的合理方法,您否则可能会使用ThreadPoolExecutor。

我假设,在涉及相当小的CPU绑定任务时,ForkJoinPool是最佳选择,由于此优化,递归分解已经不再需要。 工作窃取会起作用,无论是大型还是小型任务-可以从忙碌工人的Deque尾部由另一个空闲工人抓取任务。

更新3: ForkJoinPool的可扩展性 - Akka团队对乒乓球进行基准测试并显示出色的结果。

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


我自己也曾经想过这个问题,我认为这与线程的停放和锁定有关。这就是为什么我认为环形缓冲区(Disruptor)甚至比未包装的Fork/Join(至少对于我的非递归事件任务而言)更快的原因。 - Adam Gent
@Adam Gent 我们也使用了Disruptor - 它的速度非常快。而且,RingBuffer的思想被应用在许多现代数据结构中。然而,它们在使用案例上有所不同:1. Disruptor使用1个线程-1个消费者模型进行流水线处理,不能窃取工作,而2. FJ是一个工作池 - 工作分布在多个线程(m:n)之间。 - Ivan Voroshilin
我熟悉fj-rb的区别,但我认为它们都可能受益于较少的锁定,但我并不是专家。 - Adam Gent
这是我的猜测,FJ在处理非递归并行任务时会更快,因为它的锁定比ThreadPoolExecutor少。我曾尝试通过将一些并行CPU绑定的ThreadPoolExecutorService替换为FJ来进行基准测试,但改进非常微不足道,如果有的话。 - Adam Gent
@Adam Gent,没错,线程池ExecutorServices使用的是FIFO阻塞队列,在多核CPU上扩展性较差——多个线程从头部进行取/放操作,导致争用更高,不像FJ中的双端队列。我认为,在更多的CPU上你会看到差异。对于2-4个内核,没有太大的优势。此外,正如所说,这需要适当的调整。 - Ivan Voroshilin
1个回答

19

ForkJoinPool源代码有一个很好的名为"Implementation Overview"的部分,阅读以获取终极真相。下面的解释是我对JDK 8u40的理解。

从一开始,ForkJoinPool每个工作线程都有一个工作队列(让我们称之为 "worker queues")。分叉任务被推到本地工作队列中,准备再次被工作线程弹出并执行 -- 换句话说,从工作线程的角度来看,它看起来像一个堆栈。当一个工作线程耗尽其工作队列时,它会去尝试从其他工作线程的队列中窃取任务。这就是“工作窃取”。

现在,在JDK 7u12之前(如我所记),ForkJoinPool有一个单一的全局提交队列。当工作线程用完本地任务和可窃取的任务后,它们会到那里并尝试查看是否有可用的外部工作。在这种设计中,与支持ArrayBlockingQueue的常规ThreadPoolExecutor相比没有优势。

然后,在此提交队列被识别为严重的性能瓶颈之后发生了显着变化。Doug Lea等人也将提交队列划分为条带状的。事后看来,这是一个显而易见的想法:您可以重复使用大多数可用于工作队列的机制。您甚至可以松散地将这些提交队列分发到每个工作线程中。现在,外部提交进入其中一个提交队列。然后,没有任务可处理的工作线程可以首先查看与特定工作线程相关的提交队列,然后四处闲逛查看其他提交队列。可以称之为“工作窃取”。

我看到许多工作负载因此受益。即使对于普通的非递归任务,ForkJoinPool 的这种设计优势早已被认可。许多并发用户在 concurrency-interest@ 上要求一个简单的工作窃取执行器,而不需要所有的 ForkJoinPool 奥秘。这就是为什么我们在 JDK 8 及以后有了 Executors.newWorkStealingPool() 的原因 -- 目前委托给 ForkJoinPool,但可以提供更简单的实现。


你能否详细说明一下 Executors.newWorkStealingPool()ForkJoinPool 和/或 ForkJoinPool.commonpool() 的区别? - AnV

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