【问题标题】:How to execute statements in a for-each loop in parallel in Java?如何在Java中并行执行for-each循环中的语句?
【发布时间】:2019-04-09 16:28:37
【问题描述】:

我有一段代码如下所示:

public List<Restaurant> getAllRestaurants() {
    List<Restaurant> restaurants = getRestaurants().subList(0, 7); // This takes 234 ms to execute on average.    

    // There are 7 items in the restaurants list
    for (Restaurant restaurant : restaurants) {
        PlacesAPIResponse response = callGooglePlacesAPI(restaurant); // A call to the Google API should take 520ms for a given restaurant
        restaurant.setRating(response.getRating());
    }
    return restaurants;
}

如果我如图所示在 for-each 循环中执行上述语句,我预计该方法的总时间为 234ms + (7*520)ms = 3874ms,因为语句是按顺序运行的。这太慢了,所以我想并行化 for-each 循环中的语句,以便我为列表中的每个餐厅同时调用 Google Places API。理论上,响应时间应该是234ms + max(API call for Restaurant 1, ..., API call for Restaurant 7) = 234ms + 520ms = 754ms,因为对 Google API 的调用是并行进行的。

根据this link (Java 8: Parallel FOR loop),我应该可以像这样使用parallelStream()同时执行语句:

long startTime = System.currentTimeMillis();
restaurants.parallelStream().forEach(restaurant -> {
    PlacesAPIResponse response = callGooglePlacesAPI(restaurant);
    restaurant.setRating(response.getRating());
});
long endTime = System.currentTimeMillis();
System.out.println("Calling Google Places API took " + (endTime - startTime) + " milliseconds");

这似乎为每家餐厅并行调用 Google Places API,但现在每次调用 Google Places API 似乎都需要越来越多的时间。这是我的时间戳的输出:

getRestaurants() took 234 milliseconds
Took 335 milliseconds to call Google Places API for Restaurant 1
Took 337 milliseconds to call Google Places API for Restaurant 2
Took 671 milliseconds to call Google Places API for Restaurant 3
Took 742 milliseconds to call Google Places API for Restaurant 4
Took 1086 milliseconds to call Google Places API for Restaurant 5
Took 1116 milliseconds to call Google Places API for Restaurant 6
Took 1470 milliseconds to call Google Places API for Restaurant 7
Calling Google Places API took 1473 milliseconds

1734ms 比我预期的 754ms 大得多。我已经尝试过并行流以及 ExecutorService 来同时调用 Google Places API,但我似乎无法获得所需的响应时间。谁能指出我正确的方向?谢谢。

编辑:这是我对 ExecutorService 的尝试,根据这篇文章(Is there a easy way to parallelize a foreach loop in java?)

startTime = System.currentTimeMillis();
ExecutorService exe = Executors.newFixedThreadPool(2);   // 2 can be changed of course
for (Restaurant restaurant : restaurants) {
    exe.submit(() -> {
        PlacesAPIResponse response = callGooglePlacesAPI(restaurant); // A call to the Google API should take 520ms for a given restaurant
        restaurant.setRating(response.getRating());
    });
}    

exe.shutdown();
try {
    exe.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
    e.printStackTrace();
}    

endTime = System.currentTimeMillis();
System.out.println("Calling Google Places API took " + (endTime - startTime) + " milliseconds");
return restaurants; 

这是我的时间戳的输出:

getRestaurants() took 234 milliseconds
Took 464 milliseconds to call Google Places API for Restaurant 1
Took 575 milliseconds to call Google Places API for Restaurant 2
Took 452 milliseconds to call Google Places API for Restaurant 3
Took 420 milliseconds to call Google Places API for Restaurant 4
Took 414 milliseconds to call Google Places API for Restaurant 5
Took 444 milliseconds to call Google Places API for Restaurant 6
Took 422 milliseconds to call Google Places API for Restaurant 7
Calling Google Places API took 1757 milliseconds

这个方法的响应时间仍然是234ms + 1757 ms而不是234ms + 575ms,我不明白为什么。

【问题讨论】:

    标签: java multithreading concurrency parallel-processing


    【解决方案1】:

    这里最好使用 executorService 并为它们提供任务作为单独的 Runnable()。

    或者你可以在这里使用 Future。

    【讨论】:

    • 请看我的编辑。我解释了我对 ExecutorService 所做的事情,但它仍然没有给我想要的响应时间。
    • 那么看起来@Chris 是对的,而且是 Google 让您的请求保持有序
    • 您可以创建一个仅在一个线程上运行的 ExecutorService。是的,这是正确的道路,但只是说“执行者服务”只是一个开始,而不是一个整体的答案。
    【解决方案2】:

    这是很久以前的事了,但我想原因在于您选择的线程池大小。线程池大小为 2 意味着您只能并行执行两个作业。剩余的作业排队,直到线程被释放。因此,您执行 Google Places API 的计算将类似于max(464+452+414+422, 575+420+444) = max(1752, 1439) = 1752,接近实际值。这很好解释here

    【讨论】:

      【解决方案3】:

      我猜你的瓶颈是与互联网或 Google Places 服务器的连接,而不是你的循环。服务器识别相同的 IP 地址,因此将您的请求排队以保护自己免受拒绝服务攻击。 这意味着您的循环并行运行,但互联网请求堆叠在服务器上,这就是为什么每个请求越来越需要更多时间才能得到响应和返回。 为避免这种情况,您需要类似 bot net(从不同计算机发送每个查询)之类的东西,或者 Google Places 可能会向您出售用于并行请求的特殊连接。

      【讨论】:

      • 有趣,我没想到。我如何能够测试这实际上正在发生,而不是我的循环是错误的?
      • @Warren 打印出 startTime。如果所有查询几乎同时开始,那么您的循环是并行的。但是,如果后续请求仅在前一个请求完成后才开始,那么您的循环是顺序的。您也可以使用 Thread.currentThread().getId() 打印出线程 ID。如果所有的 Id 都相同,那么它是顺序的,如果它们都是不同的,那么它是并行的。
      • 谢谢克里斯。是的,所有请求的 startTime 都相同,线程 ID 不同。所以看起来我的请求确实是并行发生的,而且 Google 正在将请求排在最后。
      • @WarrenCrasta 您可以通过将我的条目标记为已接受的答案并投票来感谢我。那太好了,因为我刚开始使用 stackoverflow 并且需要每一点。
      • 经过进一步分析,Google 似乎并没有限制我的请求。我用我开发的本地计算机上的另一个 API 替换了对 Google Place Details API 的调用,并且我看到了相同的行为。如果我弄清楚发生了什么,我会在这里发布。
      猜你喜欢
      • 2018-08-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多