工作窃取:加入递归任务需要窃取?

时间:2018-03-05 02:46:28

标签: fork-join work-stealing

我试图了解工作窃取对递归任务的影响: 工作窃取的一个优点是当前的工作者/线程可能会执行自己生成的任务;增加数据的位置。 但是,当工人加入其衍生任务时,常见情况会发生什么? 例如:

Future<String> a=pool.submit(()->doA());
b=doB();
return a.get()+b;

我认为这里的当前线程会被阻塞,因此无法从自己的队列中获取工作,因此另一名工作人员将不得不窃取这些工作。这将否认工作窃取的地方优势。但是,根据维基百科(https://en.wikipedia.org/wiki/Work_stealing) &#34;工作窃取是为严格的&#34;而设计的。并行计算的fork-join模型&#34; 在我的推理中我一定有些错误,但我找不到它。

更多细节,请考虑以下代码:

Future<String> res=pool.submit(()->{
  Future<String> a=pool.submit(()->doA());
  b=doB();
  return a.get()+b;
  });
res.get();

此代码应该在worker内部开始计算。这样的工人将产生一个新任务。然后他尝试获取此嵌套任务的结果。这个嵌套任务是如何执行的?

1 个答案:

答案 0 :(得分:2)

fork-join池为Java程序员提供了高性能,并行,细粒度的任务执行框架。
它通过分而治之解决了这个问题。将任务拆分为子任务。一个任务通过 fork()方法创建一个子任务。
当任务客户端提交/调用/执行fork联接任务时,任务进入共享队列,此共享队列用于馈送 非共享双端队列 < / strong>(又称“ deque ”),由 WorkerThread 管理。
一个或多个 WorkerThreads 被称为Fork-Join池。
WorkerThread 共享队列中拉出任务,然后他们开始处理工作(使用非共享队列)。
Fork-Join-Pool中的每个 WorkerThread (实际上是 Java线程)都在循环中运行,该循环不断扫描(子)任务执行。
目标是尝试使 WorkerThreads 尽可能繁忙,以便我们希望他们总是有事可做。
目标是最大化处理器核心利用率。
每个 WorkerThread 都有双头队列(又称“ 双端队列 ”)是其主要任务来源。
除此以外,其他共享队列过去曾用于将非分叉加入任务放入分叉加入池中,排在第一位。
deque ”由 WorkQueue (这是一个嵌套在ForkJoinPool中的Java类)实现。该类中的一些重要方法是 push(),pop()和poll()

在某些时候,该任务无法取得任何进展,因为它正在等待 join()方法完成子任务。
此联接与Java线程中的联接不同。
Java线程连接中,如果任务没有返回结果,则为阻塞,并等待该另一个线程完成。
如果在Fork-Join中 join()恰好阻塞,则 WorkerThread 会停止在当前线程上工作并开始执行子任务..
每当您在 RecursiveTask <> RecursiveAction 中始终在内部的计算方法中调用 fork() 时,在fork-join-pool中的线程上下文中运行。 如果 RecursiveTask <> RecursiveAction 的任务由 WorkerThread 运行,则调用 fork()< / em>,将新的* ForkJoinTask **推入该工作程序“ deque ”线程的头部。 它按 LIFO 的顺序将后进先出。
当我们为此任务调用 join()时,该任务将从“ deque
”(堆栈顶部)的开头弹出并在 WorkerThread 中运行至完成状态(保持运行直至完成)。

为什么我们要 LIFO ?我们为什么要在前面推动而在前面弹出呢?为了提高引用的局部性,提高缓存性能,以便您尽快进行处理,有时称为过时的新工作。

ForkJoinTask启用细粒度的数据并行性。
ForkJoinTask Java线程轻巧,它没有自己的运行时堆栈。 ForkJoinTask 将大量数据与对该数据的计算相关联。
真正的Java线程具有其自己的堆栈,寄存器以及许多其他资源,这些资源使操作系统可以在内部由线程调度程序独立地对其进行调度。

ForkJoinTask 可以在Fork-Join-Pool中的更少数量的 WorkerThreads 中运行。
WorkerThreads 的数量通常(如果未指定)是内核数量的函数。每个 WorkerThread 是一个 Java线程对象,具有您希望从普通线程获得的所有装备。

ForkJoinTask 有两种控制并行处理和合并结果的重要方法,它们是 fork() join()
fork()安排在适当的线程池中异步执行此任务。 fork()就像Thread.start()的lightversion。
fork()不会创建Java工作线程(至少不是直接创建),但是最终它将在Java线程上运行。
它不会立即开始运行,而是将子任务放在工作队列的开头。


当子任务完成时, join()返回计算结果。
Fork-Join池中的联接与经典Java线程联接不同。
Java线程用作屏障同步器,以等待另一个线程完成,然后加入它(直到另一个线程完成后才能继续)。
常规线程中的联接会阻止调用线程。
Fork-Join池中的联接不只是阻塞调用线程,而是分配了 WorkerThread 来运行挂起的子任务。
WorkerThread 遇到 join()时,它将处理其他所有子任务,直到发现目标子任务已完成。在此子任务结果完成之前, WorkerThreads 不会返回到调用方。
在fork-join任务中的Join不会阻塞,它保留当前任务,因此只有在 join()创建的子任务完成后,计算才能继续。
WorkerThread 指出,该任务被阻塞直到子任务完成,因此它开始在该子任务上工作。

WorkerThread LIFO 顺序处理其自身的“双端队列”,方法是从其自身的“ 双端队列”弹出(子)任务。 em> ”。

工作稳定
WorkerThread 没有其他事情可做时-“ idle”。如果 WorkerThread 自己的队列为空,它将去尝试“窃取”一个子-从其他繁忙线程“ deque ”的尾部执行的任务,该任务是随机选择的,目的是最大程度地提高核心利用率。
任务是按 FIFO 顺序“被盗”的,因为较旧的被盗任务可能会提供较大的工作量。
Push() pop()仅由拥有的工作线程调用(在 deque 的顶部>”),这就是他们使用免等待的“ C ompare- A nd- S wap” CAS 操作。 CAS 是自动检查并设置内存锁定值的硬件级别-它从不阻塞。 push() pop()的锁定非常轻巧。
可以从另一个线程调用 Poll()作为子任务来“窃取”。当我们调用 poll()时,这是因为已经随机分配了另一个线程,以尝试以 FIFO 的顺序从此双端队列的末尾“窃取”子任务。 Poll()由另一个线程启动,因此它可能并不总是不需要等待,因此有时它必须“屈服”并返回并稍后再试。 “窃取”的速度很快,但可能不如推和弹出的速度快。