在Java8 parallelStream()中使用I / O + ManagedBlocker有什么问题吗?

时间:2016-05-29 17:06:45

标签: java java-stream

默认" paralellStream()"在Java 8中使用公共ForkJoinPool,如果在提交任务时公共池线程用尽,则可能是延迟问题。但是,在许多情况下,有足够的CPU功率可用且任务足够短,因此这不是问题。如果我们确实有一些长期运行的任务,这当然需要仔细考虑,但对于这个问题,让我们假设这不是问题。

但是,尽管有足够的CPU功率可用,但填充ForkJoinPool的I / O任务实际上并不执行任何CPU限制工作是引入瓶颈的一种方法。 I understood that。然而,这就是我们ManagedBlocker的含义。因此,如果我们有I / O任务,我们应该只允许ForkJoinPoolManagedBlocker内管理它。听起来非常简单。但令我惊讶的是,使用ManagedBlocker是一个相当复杂的API,因为它很简单。毕竟我认为这是一个常见的问题。所以我只是构建了一个简单的实用方法,使ManagedBlocker易于用于常见情况:

public class BlockingTasks {

    public static<T> T callInManagedBlock(final Supplier<T> supplier) {
        final SupplierManagedBlock<T> managedBlock = new SupplierManagedBlock<>(supplier);
        try {
            ForkJoinPool.managedBlock(managedBlock);
        } catch (InterruptedException e) {
            throw new Error(e);
        }
        return managedBlock.getResult();
    }

    private static class SupplierManagedBlock<T> implements ForkJoinPool.ManagedBlocker {
        private final Supplier<T> supplier;
        private T result;
        private boolean done = false;

        private SupplierManagedBlock(final Supplier<T> supplier) {
            this.supplier = supplier;
        }

        @Override
        public boolean block() {
            result = supplier.get();
            done = true;
            return true;
        }

        @Override
        public boolean isReleasable() {
            return done;
        }

        public T getResult() {
            return result;
        }
    }
}

现在,如果我想在paralell中下载几个网站的html代码,我可以像这样在没有I / O的情况下造成任何麻烦:

public static void main(String[] args) {
    final List<String> pagesHtml = Stream
        .of("https://google.com", "https://stackoverflow.com", "...")
        .map((url) -> BlockingTasks.callInManagedBlock(() -> download(url)))
        .collect(Collectors.toList());
}

我有点惊讶的是,上面没有像BlockingTasks这样的类附带Java(或者我没有找到它?),但它并不难建立。

当我google for&#34; java 8 parallel stream&#34;我在前四个结果中得到那些声称由于I / O问题而使用Java / Join糟透了Java的文章:

我在某种程度上改变了我的搜索条件,虽然有很多人在抱怨生活多么可怕,但我发现没有人会像上面那样谈论解决方案。由于我不觉得Marvin(大脑像行星一样)和Java 8已经存在了很长一段时间,我怀疑我在那里提出的建议存在严重错误。

我在一起进行了一次小测试:

public static void main(String[] args) {
    System.out.println(DateTimeFormatter.ISO_LOCAL_TIME.format(LocalTime.now()) + ": Start");
    IntStream.range(0, 10).parallel().forEach((x) -> sleep());
    System.out.println(DateTimeFormatter.ISO_LOCAL_TIME.format(LocalTime.now()) + ": End");
}

public static void sleep() {
    try {
        System.out.println(DateTimeFormatter.ISO_LOCAL_TIME.format(LocalTime.now()) + ": Sleeping " + Thread.currentThread().getName());
        Thread.sleep(10000);
    } catch (InterruptedException e) {
        throw new Error(e);
    }
}

我跑了,得到了以下结果:

18:41:29.021: Start
18:41:29.033: Sleeping main
18:41:29.034: Sleeping ForkJoinPool.commonPool-worker-1
18:41:29.034: Sleeping ForkJoinPool.commonPool-worker-2
18:41:29.034: Sleeping ForkJoinPool.commonPool-worker-5
18:41:29.034: Sleeping ForkJoinPool.commonPool-worker-4
18:41:29.035: Sleeping ForkJoinPool.commonPool-worker-6
18:41:29.035: Sleeping ForkJoinPool.commonPool-worker-3
18:41:29.035: Sleeping ForkJoinPool.commonPool-worker-7
18:41:39.034: Sleeping main
18:41:39.034: Sleeping ForkJoinPool.commonPool-worker-1
18:41:49.035: End

因此,在我的8 CPU计算机上,ForkJoinPool自然地选择了8个线程,完成了前8个任务,最后完成了最后两个任务,这意味着这需要20秒,如果有其他任务排队,则池可能仍然有没有使用明显空闲的CPU(过去10秒内除了6个核心)。

然后我用了......

IntStream.range(0, 10).parallel().forEach((x) -> callInManagedBlock(() -> { sleep(); return null; }));

......而不是......

IntStream.range(0, 10).parallel().forEach((x) -> sleep());

...并得到以下结果:

18:44:10.93: Start
18:44:10.945: Sleeping main
18:44:10.953: Sleeping ForkJoinPool.commonPool-worker-7
18:44:10.953: Sleeping ForkJoinPool.commonPool-worker-1
18:44:10.953: Sleeping ForkJoinPool.commonPool-worker-6
18:44:10.953: Sleeping ForkJoinPool.commonPool-worker-3
18:44:10.955: Sleeping ForkJoinPool.commonPool-worker-2
18:44:10.956: Sleeping ForkJoinPool.commonPool-worker-4
18:44:10.956: Sleeping ForkJoinPool.commonPool-worker-5
18:44:10.956: Sleeping ForkJoinPool.commonPool-worker-0
18:44:10.956: Sleeping ForkJoinPool.commonPool-worker-11
18:44:20.957: End

在我看来,这样的工作,额外的线程开始补偿我的模拟&#34;阻止I / O操作&#34; (睡觉)。时间减少到10秒,我想如果我排队更多的任务,那些仍然可以使用可用的CPU功率。

如果I / O操作包含在ManagedBlock

中,此解决方案或通常在流中使用I / O是否有任何问题?

1 个答案:

答案 0 :(得分:7)

简而言之,是的,您的解决方案存在一些问题。它肯定改进了在并行流中使用阻塞代码,并且一些第三方库提供了类似的解决方案(例如,参见jOOλ库中的Blocking类)。但是,此解决方案不会更改Stream API中使用的内部拆分策略。 Stream API创建的子任务数由$ grep -A3 exception server.log server.log server.log:exception server.log-1 server.log-2 server.log-3 -- server.log:exception server.log-1 server.log-2 server.log-3 class:

中的预定义常量控制
$ awk -v A=3 '/exception/{f=A+1;if(x)print"---";x=1} f{print FILENAME (f==A+1?":":"-") $0;f--}' server.log server.log
server.log:exception
server.log-1
server.log-2
server.log-3
---
server.log:exception
server.log-1
server.log-2
server.log-3

正如您所看到的,它比普通池并行性(默认为CPU核心数)大四倍。真正的分裂算法有点棘手,但即使所有任务都阻塞,你也不能超过4x-8x任务。

例如,如果您有8个CPU核心,那么AbstractTask测试可以很好地工作到/** * Default target factor of leaf tasks for parallel decomposition. * To allow load balancing, we over-partition, currently to approximately * four tasks per processor, which enables others to help out * if leaf tasks are uneven or some processors are otherwise busy. */ static final int LEAF_TARGET = ForkJoinPool.getCommonPoolParallelism() << 2; (因为32 = 8 * 4)。但是对于Thread.sleep(),您将有32个并行任务,每个处理两个输入数字,因此整个处理需要20秒,而不是10秒。