【问题标题】:Work Stealing: join a recursive task requires stealing?工作窃取:加入递归任务需要窃取?
【发布时间】:2018-03-05 02:46:28
【问题描述】:

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

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

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

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

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

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

【问题讨论】:

  • 你自己试过了吗?
  • 我不确定“如何”尝试,因为唯一认为应该改变的是性能。我试图打印当前线程名称,并且我可以设法为顶级线程和“a”线程获取“相同”名称和相同身份哈希码(使用我的示例名称)所以,它看起来像以有效的方式工作..但是,我不明白它是如何工作的。可能是如果一个工作人员试图进入一个甚至没有开始并且在自己的队列中的未来,那么它会接受它并按顺序执行它??
  • 没有看到你在做什么,我不能说什么。现在,提交的工作进入提交队列。任何线程都可以完成这项工作。通常,提交线程执行它自己的工作,但并非总是如此。这就是为什么我需要看看你在做什么。
  • 我试图让问题更清楚!请考虑我的更新版本。
  • 我仍然无法按照您的操作进行操作,因为我无法编译它。见sscce.org

标签: fork-join work-stealing


【解决方案1】:

fork-join 池为 Java 程序员提供了一个高性能、并行、精细的任务执行框架。

它通过分而治之来解决问题。将任务拆分为子任务。任务通过 fork() 方法创建子任务。

当任务客户端提交/调用/执行一个fork join任务时,该任务进入一个共享队列,这个共享队列用来喂非共享双端队列(又名“deque”)由 WorkerThread 管理。

一个或多个 WorkerThreads 称为 Fork-Join 池。

WorkerThread共享队列 中拉出任务,然后他们开始处理工作(使用非共享队列) .

Fork-Join-Pool 中的每个 WorkerThread(实际上是一个 Java 线程)都在一个循环中运行,该循环不断扫描 (sub -)要执行的任务。

我们的目标是尽量让 WorkerThreads 尽可能忙碌,因此我们希望他们总是有事可做。

目标是最大化处理器核心利用率。

每个WorkerThread都有自己的双端队列(又名“deque ") 作为其主要任务来源。

除此之外,其他共享队列曾经将非分叉加入任务放入分叉加入池中,排名第一。

deque”由 WorkQueue(嵌套在 ForkJoinPool 中的 Java 类)实现。该类中的一些重要方法是 push()、pop() 和 poll()

在某些时候,任务无法取得任何进展,因为它正在等待通过 join() 方法完成的子任务。

此连接不同于 Java 线程中的连接。

Java Thread Join中,如果一个任务没有返回结果,就会阻塞,并等待其他线程完成。

如果在 Fork-Join 中 join() 发生阻塞,则 WorkerThread 停止在当前线程上工作并开始执行子任务..

每当您在 RecursiveTaskRecursiveAction 内部调用 fork() 内部计算方法时它始终在 fork-join-pool 中的线程上下文中运行。

如果 RecursiveTaskRecursiveAction 正在运行的任务由 WorkerThread 调用 fork (),那个新的 ForkJoinTask* 被推到那个工人“deque”线程的头部。

它以 LIFO 顺序推动它,后进先出。

当我们为此任务调用 join() 时,该任务将从“deque”的头部弹出(顶部堆栈)并在 WorkerThread 中运行到完成(继续运行直到完成)。

我们为什么要LIFO?为什么我们在前面推,在前面弹出?为了提高引用的局部性,提高缓存性能,让你尽快得到处理,有时也称为新鲜工作。

ForkJoinTask 支持细粒度的数据并行。

ForkJoinTaskJava 线程 更轻量,它没有自己的运行时堆栈。

ForkJoinTask 将数据块与对该数据的计算相关联。

一个真正的 Java 线程有它自己的堆栈、寄存器和许多其他资源,这些资源允许它被操作系统内部的线程调度程序独立管理。

大量的 ForkJoinTask 可以在 Fork-Join-Pool 中少得多的 WorkerThreads 中运行。

WorkerThreads 的数量通常(如果未指定)是内核数量的函数。每个 WorkerThread 都是一个 Java 线程 对象,其中包含您对普通线程的期望。

ForkJoinTask 有两个重要的方法来控制并行处理和合并结果,它们是 fork()join()

fork() 安排在适当的线程池中异步执行此任务。 fork() 就像 Thread.start() 的轻量版。

fork() 不会创建 Java 工作线程(至少不会直接创建),但最终会在 Java 线程上运行。

它不会立即开始运行,而是将子任务放在工作队列的头部。

A join() 在子任务完成时返回计算。 Fork-Join 池中的加入不同于经典的 Java 线程加入。 Java 线程用作屏障同步器,等待另一个线程完成,然后加入它(直到另一个线程完成后才能继续)。

普通线程中的连接会阻塞调用线程。

Fork-Join 池中的连接不会简单地阻塞调用线程,而是分配 WorkerThread 来运行待处理的子任务。

WorkerThread 遇到 join() 时,它会处理任何其他子任务,直到它注意到目标子任务已完成。 WorkerThreads 在此子任务结果完成之前不会返回给调用者。

fork-join 任务中的 Join 不是阻塞的,它持有当前任务,因此只有在 join() 创建的子任务完成后才能继续计算。 p>

WorkerThread 发现,该任务在子任务完成之前被阻塞,因此它开始处理子任务。

WorkerThread 通过从它自己的“deque 中弹出(子)任务,以 LIFO 顺序处理它自己的“deque”强>”。

工作窃取
当一个 WorkerThread 没有其他事情可做时 - “空闲”。如果 WorkerThread 自己的队列是空的,它会去尝试“窃取”一个子- 来自其他繁忙线程“deque”尾部的任务,该线程是随机选择的,以最大限度地提高核心利用率。

任务按 FIFO 顺序“被盗”,因为较旧的被盗任务可能会提供大量工作单元。

Push()pop() 仅由拥有的工作线程调用(到“deque ") 这就是他们最高效的原因,他们使用无等待“Compare-And-Swap” CAS 操作。 CAS 是在内存中原子检查和设置 lock 值的硬件级别 - 它从不阻塞。 push()pop() 有一个非常轻量级的锁定。

Poll() 可以从另一个线程调用以“窃取”作为子任务。当我们调用 poll() 时,这是因为另一个线程已被随机分配以尝试以 FIFO 顺序从该双端队列末尾“窃取”子任务。 Poll() 是由另一个线程发起的,因此它可能并不总是免等待,因此有时它必须“让步”并稍后再试。 “偷”速度很快,但可能不如推拉快。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-01-31
    • 2016-12-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多