ExecutorService:如何在线程中完成同步障碍时防止线程不足

时间:2017-08-23 12:36:25

标签: java multithreading concurrency java.util.concurrent

我遇到了一个麻烦,找不到干净的解决方案。我会尽量详细解释。

我有一个树状的结构:

NODE A
    NODE A.1
    NODE A.2
        NODE A.2.a
        NODE A.2.b
    NODE A.3
        NODE A.3.a
        NODE A.3.b
        NODE A.3.c
NODE B
    NODE B.1
    NODE B.2

我需要处理根节点:

 public void process(final Node node) { ... }

节点的过程涉及两件事:

- some database queries
- the process of all children of these nodes

换句话说,处理完NODE.2.aNODE.2.b后,就可以处理NODE.2。我以递归的方式处理节点,没什么了不起的。

到目前为止,这么好。现在我想声明一个具有固定线程数的全局执行程序服务。我想并行处理节点的子节点。因此,NODE.2.aNODE.2.b可以在各自的线程中处理。代码看起来像这样:

// global executor service, shared between all process(Node) calls
final ExecutorService service = Executors.newFixedThreadPool(4);

public void process(final Node node) {
    // database queries
    ...

    // wait for children to be processed
    final CountDownLatch latch = new CountDownLatch(node.children().size());

    for (final Node child : node.children()) {
        service.execute(() -> {
             process(child);
             latch.countDown();
        });
    }
    latch.await();
}

这里的问题是:当达到某个深度时,所有线程都在latch.await()中停止。我们已经陷入线索饥饿的境地。

这可以通过使执行者服务无限制来轻松解决,但我不喜欢这个选项。我想控制active个线程的数量。就我而言,active个线程的数量将等于核心数量。拥有更多active个线程会引入从一个线程到另一个线程的交换,我想避免这种情况。

如何解决这个问题?

4 个答案:

答案 0 :(得分:2)

技术上是一个僵局。 我们首先想到死锁,因为当两个或多个进程正在等待锁时,但在这种情况下,线程正在彼此等待。 有趣的是它是一种产生自我僵局的奇怪野兽的方式。如果你有一个线程的池,它将提交任务并等待它们但从不执行,因为它等待自己完成执行它们!

标准答案称为工作窃取线程池'。 我遇到了完全相同的问题,没有那段词汇需要很长时间才能找到关于我快速解决的任何信息,这是在完全并发递归中执行的任何递归算法中的常见问题。

好的,这是它如何工作的草图。 使用一个技巧创建一个相当标准的线程池。 当一个线程到达一个无法在没有排队项结果的情况下继续运行的点时,检查该项是否已由另一个线程启动,如果它没有在当前线程中执行它(而不是等待),否则等待执行该项目的线程完成它。

它非常简单,但可能需要您构建自己的线程池类,因为现成的解决方案通常不支持它。

当然要注意,典型的非平凡递归算法会分解成比并行处理单元更多的子任务。 因此,将子任务排入某个级别然后在单个线程中执行余数可能是有意义的。

除非将项目排队和排队很便宜,否则您可以花时间将项目放入队列中以取回它们。这些操作可以在线程中序列化并减少并行性。很难使用无锁队列,因为它们通常不允许从队列中间提取(这里需要)。你应该限制平行的深度。

另一方面,请注意,在当前正在执行的线程中执行任务所涉及的任务交换开销少于停止当前线程和(在操作系统级别)交换另一个工作线程。

这是一个提供WorkStealingThreadPool的Java资源。 请注意我没有努力评估实施情况,也没有提供任何建议。在这种情况下,我一直在使用C ++工作,或者很乐意与我分享template

https://zeroturnaround.com/rebellabs/fixedthreadpool-cachedthreadpool-or-forkjoinpool-picking-correct-java-executors-for-background-tasks/

另请参阅维基百科所说的内容:https://en.wikipedia.org/wiki/Work_stealing

答案 1 :(得分:1)

我不会使用容易出错的多个CountDownLatch-es。据我所知,您希望并行地从树叶节点到父节点遍历树。

我会使用简单的BFS / DFS遍历树(你不需要递归算法),一旦找到没有子节点的叶子,就将这个节点放入阻塞队列。

队列可以被第二个线程轮询,该线程将使用固定数量的线程将新任务安排到执行程序服务。

一旦执行程序线程完成处理从队列中取出的当前节点,执行程序线程将检查当前节点是否具有父节点。如果有父节点,请再次将其放入队列中。您还应检查父项是否已被处理。

这样的事情:

can't resolve reference #/event.json from id

答案 2 :(得分:1)

看起来你想要一个替代递归。另一种选择是分散 - 聚集。提交分叉所有任务的请求。当NODE.2执行时,由于子节点尚未完成,因此返回简单。当最后一个任务完成(2.1,2.b)时,完成处理开始处理NODE.2。

我维护一个Data Parallel开源产品,可以做到这一点。而不是获取整个产品,只需下载文档(http://coopsoft.com/JavaDoc.html)并查看文件:Manual / DynamicJoin.html。如果这样可以解决您的问题,那么您就可以获取产品。

答案 3 :(得分:0)

使用

可以实现您真正想要的
int cores = Runtime.getRuntime().availableProcessors();
ExecutorService executorService = Executors.newFixedThreadPool(cores);

还要记住getActiveCount()的javadoc表示它是一个近似数字。