[英]How to efficiently use CompletableFuture to map async task per input
I want to return map consisting mapping of all keys to value being API response to those keys.我想返回 map,包括所有键到值的映射,即 API 对这些键的响应。 I am using CompletableFuture
and Guava
for this.我为此使用CompletableFuture
和Guava
。 Below is my attempt.下面是我的尝试。 Is there any other standard way to achieve the same with Java 8 and threading APIs?是否有任何其他标准方法可以通过 Java 8 和线程 API 实现相同的目标?
Map being id -> apiResponse(id)
. Map 是id -> apiResponse(id)
。
public static List<String> returnAPIResponse(Integer key) {
return Lists.newArrayList(key.toString() + " Test");
}
public static void main(String[] args) {
List<Integer> keys = Lists.newArrayList(1, 2, 3, 4);
List<CompletableFuture<SimpleEntry<Integer, List<String>>>> futures = keys
.stream()
.map(key -> CompletableFuture.supplyAsync(
() -> new AbstractMap.SimpleEntry<>(key, returnAPIResponse(key))))
.collect(Collectors.toList());
System.out.println(
futures.parallelStream()
.map(CompletableFuture::join)
.collect(Collectors.toList()));
}
There is an interesting behavior here that I will try my best to explain.这里有一个有趣的行为,我会尽力解释。 Let's start simple, let's forget about CompletableFuture
for a second and simply do this with a plain parallelStream
, with a minor debugging step added:让我们从简单的开始,让我们暂时忘记CompletableFuture
并简单地使用普通的parallelStream
来执行此操作,并添加了一个小的调试步骤:
List<Integer> keys = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
Map<Integer, List<String>> result =
keys.parallelStream()
.map(x -> new AbstractMap.SimpleEntry<>(x, returnAPIResponse(x)))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
System.out.println("parallelism : " + pool.getParallelism() + " current : " + pool.getPoolSize());
On my machine, this prints:在我的机器上,打印:
parallelism : 11 current : 11
I assume you already know that actions of parallelStream
are executes in the common
ForkJoinPool
.我假设您已经知道parallelStream
的操作是在common
的ForkJoinPool
中执行的。 It is probably also obvious what that output means: 11 threads
were available and all of them were used. output 的含义可能也很明显: 11 threads
可用并且全部使用。
I'll slightly modify your example now:我现在将稍微修改您的示例:
List<Integer> keys = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
ForkJoinPool pool = ForkJoinPool.commonPool();
ExecutorService supplyPool = Executors.newFixedThreadPool(2);
Map<Integer, List<String>> result =
keys.parallelStream()
.map(x -> CompletableFuture.supplyAsync(
() -> new AbstractMap.SimpleEntry<>(x, returnAPIResponse(x)),
supplyPool
))
.map(CompletableFuture::join)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
System.out.println("parallelism : " + pool.getParallelism() + " current : " + pool.getPoolSize());
It's actually just one important change, I'll let your supplyAsync
run in its own thread pool;这实际上只是一个重要的变化,我会让你的supplyAsync
在它自己的线程池中运行; the rest is the same. rest 是一样的。 Running this, reveals:运行它,显示:
parallelism : 11 current : 16
Surprise.惊喜。 More threads were created then what we wanted?创建了更多线程然后我们想要什么? Well, the documentation of getPoolSize
says that:好吧, getPoolSize
的文档说:
Returns the number of worker threads that have started but not yet terminated.返回已启动但尚未终止的工作线程数。 The result returned by this method may differ from getParallelism when threads are created to maintain parallelism when others are cooperatively blocked .此方法返回的结果可能与 getParallelism 在创建线程以在其他线程被协作阻塞时保持并行性时有所不同。
The blocking in your case happens via map(CompletableFuture::join)
.您的情况下的阻塞是通过map(CompletableFuture::join)
发生的。 You have effectively blocked a worker thread from ForkJoinPool
and it compensates that by spinning another one.您已经有效地阻止了来自ForkJoinPool
的工作线程,它通过旋转另一个工作线程来弥补这一点。
If you do not want to get into such a surprise:如果您不想遇到这样的意外:
List<CompletableFuture<AbstractMap.SimpleEntry<Integer, List<String>>>> list =
keys.stream()
.map(x -> CompletableFuture.supplyAsync(
() -> new AbstractMap.SimpleEntry<>(x, returnAPIResponse(x)),
supplyPool
))
.collect(Collectors.toList());
CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).join();
Map<Integer, List<String>> result =
list.stream()
.map(CompletableFuture::join)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Because there is no join
on the worker threads of ForJoinPool
, you can drop parallelStream
.因为ForJoinPool
的工作线程上没有join
,所以您可以删除parallelStream
。 Then I still block to get the result via:然后我仍然阻止通过以下方式获得结果:
CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).join();
but there will be no compensating threads generated.但不会产生补偿线程。 And because CompletableFuture.allOf
returns a CompletableFuture<Void>
, I need to stream again to get the results.而且因为CompletableFuture.allOf
返回CompletableFuture<Void>
,我需要再次 stream 来获得结果。
Don't let that .map(CompletableFuture::join)
in the last stream operation fool you, there is no blocking because of the previous CompletableFuture::allOf
, which already blocked and waited for all tasks to finish.不要让最后一个 stream 操作中的.map(CompletableFuture::join)
欺骗你,因为之前的CompletableFuture::allOf
已经阻塞并等待所有任务完成,所以没有阻塞。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.