Java 具有 non-recursive 个任务的 ForkJoinPool,work-stealing 有效吗?

Java ForkJoinPool with non-recursive tasks, does work-stealing work?

我想通过以下方法将 Runnable 任务提交到 ForkJoinPool:

forkJoinPool.submit(Runnable task)

注意,我用的是JDK7.

在引擎盖下,它们被转换为 ForkJoinTask objects。 我知道 ForkJoinPool 在递归地将任务拆分为较小的任务时效率很高。

问题:

如果没有递归,work-stealing在ForkJoinPool中是否仍然有效?

在这种情况下值得吗?

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

更新二: Doug Lea 在 Concurrency JSR-166 Interest 组中写道,对此给出了提示:

This also greatly improves throughput when all tasks are async and submitted to the pool rather than forked, which becomes a reasonable way to structure actor frameworks, as well as many plain services that you might otherwise use ThreadPoolExecutor for.

我想,当涉及到相当小的 CPU-bound 任务时,ForkJoinPool 是可行的方法,这要归功于这种优化。重点是这些任务已经很小了,不需要递归分解。 Work-stealing 工作,无论是大任务还是小任务 - 任务可以被另一个空闲的 worker 从繁忙的 worker 的 Deque 尾巴中抢走。

更新 3: Scalability of ForkJoinPool - ping-pong 的 Akka 团队进行的基准测试显示了很好的结果。

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

ForkJoinPool 源代码有一个很好的部分叫做 "Implementation Overview",请阅读以获取最终真理。下面的解释是我对JDK8u40.

的理解

从第一天开始,ForkJoinPool 每个工作线程都有一个工作队列(我们称它们为 "worker queues")。分叉的任务被推入本地工作队列,准备再次被工作人员弹出并执行——换句话说,从工作线程的角度来看,它看起来像一个堆栈。当一个 worker 耗尽其 worker 队列时,它会绕过并尝试从其他 worker 队列中窃取任务。即"work stealing"

现在,在 (IIRC) JDK 7u12 之前,ForkJoinPool 有一个全局 提交队列 。当工作线程 运行 退出本地任务以及要窃取的任务时,它们到达那里并尝试查看是否有外部工作可用。在这种设计中,相对于由 ArrayBlockingQueue 支持的常规 ThreadPoolExecutor 没有任何优势。

此后情况发生了很大变化。在这个提交队列被确定为严重的性能瓶颈之后,Doug Lea 等人。也对提交队列进行条带化。事后看来,这是一个显而易见的想法:您可以重用大部分可用于工作队列的机制。您甚至可以为每个工作线程松散地分配这些提交队列。现在,外部提交进入提交队列之一。然后,没有工作可做的工作人员可以先查看与特定工作人员关联的提交队列,然后四处走动查看其他工作人员的提交队列。也可以叫 that "work stealing"。

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