Java8 - 处理Stream的惯用方法<callable <...>&gt;并行交付给非线程安全的消费者?

时间:2016-12-31 23:28:48

标签: java parallel-processing java-8 java-stream

假设我有Stream<Callable<SomeClass>> stream;。该流正在访问超过一百万个不适合内存的对象。

将此转换为Stream<SomeClass>的惯用方法是什么,以确保Callable::call在被传递给非线程安全的消费者之前并行执行.sequential().forEach()(可能通过调用ExecutionService或其他一些瓶颈机制)?

即。并行处理流,但顺序传递输出(随机顺序ok,只要它是单线程)。

我知道我可以通过在原始流和消费者之间设置Queue public void closePopup() throws InterruptedException{ Thread.sleep(1000); Actions action=new Actions(driver); action.keyDown(Keys.ESCAPE).keyUp(Keys.ESCAPE).build().perform(); 来做我想做的事。但这似乎是很多代码,是否有一个神奇的单行?

5 个答案:

答案 0 :(得分:1)

您仍然可以使用ExecutorService进行并行化。像这样:

ExecutorService service = Executors.newFixedThreadPool(4);
stream.map(c -> service.submit(c)).map(future -> {
    try {               
        return future.get(); //retrieve callable result
    } catch (InterruptedException | ExecutionException ex) {        
        //Exception handling    
        throw new RuntimeException(ex);         
    }
});

您可以按顺序处理生成的Stream<SomeClass>

如果您直接在Stream<Future<SomeClass>>上使用forEach / forEachOrdered,则可以在当前未来完成时直接处理生成的SomeClass - 对象(与使用invokeAll()时阻止的对象不同每项任务都已完成)。

如果您想按照可用的确切顺序处理可调用的结果,则必须使用CompletionService,因为必要的调用,所以Future<SomeClass> f = completionService.take()不能与单个流操作链一起使用提交callables后ExecutorService

修改

在流中使用Callable无法按照上面显示的方式运行,因为每个future.get()都是通过Callables一个接一个地提交和请求的。

我发现了一个可能的副作用更重的解决方案,将TaskMapper划分为固定的并行化块。

我使用类Callables作为映射函数来提交class TaskMapper implements Function<Callable<Integer>, List<Future<Integer>>>{ private final ExecutorService service; private final int chunkSize; private List<Future<Integer>> chunk = new ArrayList<>(); TaskMapper(ExecutorService service, int chunkSize){ this.service = service; this.chunkSize = chunkSize; } @Override public List<Future<Integer>> apply(Callable<Integer> c) { chunk.add(service.submit(c)); if(chunk.size() == chunkSize){ List<Future<Integer>> fList = chunk; chunk = new ArrayList<>(); return fList; }else{ return null; } } List<Future<Integer>> getChunk(){ return chunk; } } 并将它们映射到块:

ExecutorService service = Executors.newFixedThreadPool(4);
TaskMapper taskMapper = new TaskMapper(service, 4);
stream.map(taskMapper)
    .filter(fl -> fl != null) //filter for the chunks
    .flatMap(fl -> fl.stream()) //flat-map the chunks to futures
    .map(future -> {
        try {               
            return future.get();
        } catch (InterruptedException | ExecutionException ex) {    
            throw new RuntimeException(ex);
        }
    });  
//process the remaining futures  
for(Future<Integer> f : taskMapper.getChunk()){
    try {               
        Integer i = f.get();
        //process i
    } catch (InterruptedException | ExecutionException ex) {    
        //exception handling
    }
}

这是流操作链的样子:

TaskMapper

这可以如下工作:Spliterator每次将4个callats提交给服务并将它们映射到一块未来(没有null)。这是通过每次映射到null获得第1,第2和第3个可调用来解决的。例如,Integer可以由虚拟对象替换。将期货映射到结果的映射函数等待块的每个未来的结果。我在我的示例中使用SomeClass而不是chunkSize。当映射当前块中的所有期货结果时,将创建并并行化新块。最后,如果流中的元素数量不能由TaskMapper(在我的示例中为4)分割,则必须从Spliterator检索剩余的未来并在流外部处理。 / p>

这个结构适用于我进行的测试,但我知道由于副作用,状态良好和流的未定义评估行为,它可能很脆弱。

<强> EDIT2:

我使用自定义public class ExecutorServiceSpliterator<T> extends AbstractSpliterator<Future<T>>{ private final Spliterator<? extends Callable<T>> srcSpliterator; private final ExecutorService service; private final int chunkSize; private final Queue<Future<T>> futures = new LinkedList<>(); private ExecutorServiceSpliterator(Spliterator<? extends Callable<T>> srcSpliterator) { this(srcSpliterator, Executors.newFixedThreadPool(8), 30); //default } private ExecutorServiceSpliterator(Spliterator<? extends Callable<T>> srcSpliterator, ExecutorService service, int chunkSize) { super(Long.MAX_VALUE, srcSpliterator.characteristics() & ~SIZED & ~CONCURRENT); this.srcSpliterator = srcSpliterator; this.service = service; this.chunkSize = chunkSize; } public static <T> Stream<T> pipeParallelized(Stream<? extends Callable<T>> srcStream){ return getStream(new ExecutorServiceSpliterator<>(srcStream.spliterator())); } public static <T> Stream<T> pipeParallelized(Stream<? extends Callable<T>> srcStream, ExecutorService service, int chunkSize){ return getStream(new ExecutorServiceSpliterator<>(srcStream.spliterator(), service, chunkSize)); } private static <T> Stream<T> getStream(ExecutorServiceSpliterator<T> serviceSpliterator){ return StreamSupport.stream(serviceSpliterator, false) .map(future -> { try { return future.get(); } catch (InterruptedException | ExecutionException ex) { throw new RuntimeException(ex); } } ); } @Override public boolean tryAdvance(Consumer<? super Future<T>> action) { boolean didAdvance = true; while((didAdvance = srcSpliterator.tryAdvance(c -> futures.add(service.submit(c)))) && futures.size() < chunkSize); if(!didAdvance){ service.shutdown(); } if(!futures.isEmpty()){ Future<T> future = futures.remove(); action.accept(future); return true; } return false; } }

从上一个编辑中创建了一个版本的构造
pipeParallelized()

此类提供函数(Callable),它采用Spliterators流 - 元素并行执行它们,然后输出包含结果的顺序流。允许Splitterator为有状态。因此,希望此版本不会违反任何流操作约束。这就是ExecutorServiceSpliterator.pipeParallelized(stream); 可以使用的方式(靠近“神奇的oneliner”):

Callables

这一行采用stream ExecutorServiceSpliterator的流并行化它们的执行并返回一个包含结果的顺序流(管道发生延迟 - &gt;应该可以处理数百万个callables),可以处理进一步定期进行流操作。

CompletionService的实施非常基础。它应该主要说明如何在原则上完成。可以优化服务的重新供应和结果的检索。例如,如果允许生成的流无序,则可以使用@Transactional def update(Subsidiary subsidiary,User user) { if (subsidiary == null) { transactionStatus.setRollbackOnly() notFound() return } if (subsidiary.hasErrors()) { transactionStatus.setRollbackOnly() respond subsidiary.errors, view:'edit' return } user.save flush:true user.addToSubsidiaries(subsidiary) user.save flush:true subsidiary.addToPhones(phone) subsidiary.save flush:true request.withFormat { form multipartForm { flash.message = message(code: 'default.updated.message', args: [message(code: 'subsidiary.label', default: 'Subsidiary'), subsidiary.id]) redirect subsidiary } '*'{ respond subsidiary, [status: OK] } } }

答案 1 :(得分:0)

您正在寻求一种惯用的解决方案。不鼓励在其行为参数中具有副作用的流(在流的javadoc中明确说明)。

因此惯用解决方案基本上是ExecutorService + Futures和一些循环/ forEach()。如果您有一个Stream作为参数,只需将其转换为带有标准收集器的List。

类似的东西:

    ExecutorService service = Executors.newFixedThreadPool(5);
    service.invokeAll(callables).forEach( doSomething );
    // or just
    return service.invokeAll(callables);

答案 2 :(得分:0)

第一个例子:

ExecutorService executor = Executors.newWorkStealingPool();

List<Callable<String>> callables = Arrays.asList(
    () -> "job1", 
    () -> "job2",  
    () -> "job3");

executor.invokeAll(callables).stream().map(future -> {
    return future.get();
}).forEach(System.out::println);

第二个例子:

Stream.of("1", "2", "3", "4", "", "5")
      .filter(s->s.length() > 0)
      .parallel()
      .forEachOrdered(System.out::println);

答案 3 :(得分:0)

    public static void main(String[] args) {
            testInfititeCallableStream();
        }
        private static void testInfititeCallableStream() {
            ExecutorService service = Executors.newFixedThreadPool(100);
            Consumer<Future<String>> consumeResult = (Future<String> future)->{
                try {
                    System.out.println(future.get());
                } catch (InterruptedException | ExecutionException  e) {
                    e.printStackTrace();
                } 
            };
        getCallableStream().parallel().map(callable -> service.submit(callable)).forEach(consumeResult);   

        }
    private static Stream<Callable<String>> getCallableStream() {
            Random randomWait = new Random();
            return Stream.<Callable<String>>generate(() -> 
new Callable<String>() {
                public String call() throws Exception {
                    //wait for testing
                    long time = System.currentTimeMillis();
                    TimeUnit.MILLISECONDS.sleep(randomWait.nextInt(5000));
                    return time + ":" +UUID.randomUUID().toString();
                };
            }).limit(Integer.MAX_VALUE);
        }

答案 4 :(得分:0)

其他答案都不适用于我。

我最终确定了这样的事情(伪代码):

ExecutorService executor = Executors.newWorkStealingPool();
CompletionService completor = new CompletionService(executor);
int count = stream.map(completor::submit).count();
while(count-- > 0) {
  SomeClass obj = completor.take();
  consume(obj);
}

consume(obj)循环在单个线程中按顺序执行,而各个可调用任务以异步方式通过CompletionService的多个线程。内存消耗受到限制,因为CompletionService一次只有正在进行的项目,因为有可用的线程。等待执行的Callables急切地从流中实现,但是与每次开始执行时消耗的内存相比,它的影响可以忽略不计(用例可能会有所不同)。