一个上游流为多个下游流提供

时间:2015-05-03 18:49:56

标签: java-8 java-stream

我有一个常见的Streams API问题我想解决"有效"。假设我有一个(可能非常大,可能是无限的)流。我想以某种方式预处理它,例如,过滤掉一些项目,并改变一些项目。让我们假设这个预处理是复杂的,时间和计算密集的,所以我不想做两次。

接下来,我想对项目序列执行两组不同的操作,并使用不同的流类型构造处理每个不同序列的远端。对于无限流,这将是一个forEach,对于有限的一个,它可能是一个收集器或其他。

显然,我可能会将中间结果收集到一个列表中,然后从该列表中拖出两个单独的流,分别处理每个流。这对于有限的流来说是有效的,尽管a)看起来很丑陋" b)对于一个非常大的流来说它可能是不切实际的,而对于一个无限的流来说它是不可行的。

我想我可以使用peek作为一种" tee"。然后,我可以对偷看下游的结果执行一系列处理,并以某种方式强迫消费者查看其他"其他"工作,但现在第二条路径不再是流。

我发现我可以创建一个BlockingQueue,使用peek将东西推入该队列,然后从队列中获取一个流。这似乎是一个好主意,实际上工作得很好,虽然我无法理解流是如何关闭的(它实际上是这样,但我看不出如何)。以下是示例代码:

List<Student> ls = Arrays.asList(
  new Student("Fred", 2.3F)
  // more students (and Student definition) elided ...
);

BlockingQueue<Student> pipe = new LinkedBlockingQueue<>();

ls.stream()
  .peek(s -> {
     try {
       pipe.put(s);
     } catch (InterruptedException ioe) {
       ioe.printStackTrace();
     }
   })
   .forEach(System.out::println);

   new Thread(
     new Runnable() {
       public void run() {
         Map<String, Double> map = 
           pipe.stream()
             .collect(Collectors.groupingBy(s->s.getName(),
                      Collectors.averagingDouble(s->s.getGpa())));
         map.forEach(
           (k,v)->
             System.out.println(
               "Students called " + k 
               + " average " + v));

       }
     }).start();

所以,第一个问题是:是否有更好的&#34;这样做的方法?

第二个问题,BlockingQueue上的流是如何关闭的?

干杯, 托比

1 个答案:

答案 0 :(得分:3)

有趣的问题。我首先回答第二个问题,因为它是一个更简单的问题。

  

第二个问题,BlockingQueue上的流是如何关闭的?

关闭&#34;关闭&#34;我认为你的意思是,流有一定数量的元素然后它完成,忽略了将来可能添加到队列中的任何元素。原因是队列中的流仅表示创建流时队列的当前内容。它不代表任何未来的元素,也就是那些其他线程可能会在未来添加的元素。

如果您想要一个表示队列当前和未来内容的流,那么您可以使用此other answer中描述的技术。基本上使用Stream.generate()来调用queue.take()。我不认为这是你想做的事情,所以我不会在这里进一步讨论。

现在讨论你的大问题。

您有一个对象源,您希望在其上进行一些处理,包括过滤。然后,您需要获取结果并通过两个不同的下游处理步骤发送它们。基本上你有一个生产者和两个消费者。

您必须处理的一个基本问题是如何处理不同处理步骤以不同速率发生的情况。假设我们已经解决了如何在没有流过早终止的情况下从队列中获取流的问题。如果生产者可以比消费者可以处理来自此队列的元素更快地生成元素,则队列将累积元素,直到它填满所有可用内存。

您还必须决定如何以不同的费率处理不同的消费者处理元素。如果一个消费者明显慢于另一个消费者,则可能需要缓冲任意数量的元素(这可能会填满内存),或者必须减慢更快的消费者以匹配较慢消费者的平均费率。

让我折腾一下你将如何进行的草图。但是,我不知道你的实际要求,所以我不知道这是否会令人满意。需要注意的一点是,在这种应用程序中使用并行流可能会有问题,因为并行流不能很好地处理阻塞和负载平衡。

首先,我开始使用生产者的流处理元素并将它们累积到ArrayBlockingQueue

BlockingQueue<T> queue = new ArrayBlockingQueue<>(capacity);
producer.map(...)
        .filter(...)
        .forEach(queue::put);

(请注意,put会抛出InterruptedException,因此您无法在此处放置queue::put。您必须在此处放置try-catch块,或者编写辅助方法相反。但如果发现InterruptedException,该怎么做并不明显。)

如果队列填满,这将阻止管道。要么在自己的线程中顺序运行,要么并行运行在专用线程池中,以避免阻塞公共池。

接下来,消费者:

while (true) {
    // wait until the queue is full, or a timeout has expired,
    // depending upon how frequently you want to continue
    // processing elements emitted by the producer
    List<T> list = new ArrayList<>();
    queue.drainTo(list);
    downstream1 = list.stream().filter(...).map(...).collect(...);
    downstream2 = list.stream().filter(...).map(...).collect(...);
    // deal with results downstream1 and downstream2
}

这里的关键是从生产者到消费者的切换是使用drainTo方法批量完成的,该方法将队列的元素添加到目标并以原子方式清空队列。通过这种方式,消费者不必等待生产者完成其处理(如果它是无限的则不会发生)。此外,消费者使用已知数量的数据进行操作,并且在处理过程中不会受阻。因此,如果这有用,可以想象每个消费者流并行运行。

在这里,我让消费者​​一步一步地运行。如果您希望消费者以不同的速率运行,您将不得不构建其他队列(或某些东西)来独立缓冲其工作负载。

如果消费者总体上比生产者慢,那么队列最终会被填满并被阻止,从而使生产者减慢到消费者可以接受的速度。如果消费者平均比生产者快,那么也许你不必担心消费者的相对处理率。你可以让它们循环并拾取生产者设法放入队列的任何东西,甚至让它们阻塞直到有可用的东西。

我应该说,我所概述的是一种非常简单的多阶段流水线方法。如果您的应用程序对性能至关重要,您可能会发现自己在调整内存消耗,负载平衡,提高吞吐量和减少延迟方面做了大量工作。还有其他框架可能更适合您的应用程序。例如,您可以查看LMAX Disruptor,但我自己也没有任何相关经验。