ExecutorCompletionService? Зачем нужен один, если у нас есть invokeAll?

Если мы используем ExecutorCompletionService, мы можем представить ряд задач в виде Callableи получить результат, взаимодействуя с CompletionService как queue,

Но есть и invokeAll из ExecutorService который принимает Collection задач, и мы получаем список Future чтобы получить результаты.

Насколько я могу судить, нет никакой пользы в использовании одного или другого (за исключением того, что мы избегаем for цикл с помощью invokeAll что нам придется submit задачи к CompletionService) и по сути это одна и та же идея с небольшим отличием.

Итак, почему есть 2 различных способа представить серию задач? Правильно ли я понимаю, что производительность эквивалентна? Есть ли случай, когда один из них более подходит, чем другой? Я не могу думать об одном.

4 ответа

Решение

Используя ExecutorCompletionService.poll/takeВы получаете Futures как они заканчивают, в порядке завершения (более или менее). С помощью ExecutorService.invokeAllу вас нет этой силы; Вы либо блокируете до завершения всех операций, либо указываете тайм-аут, после которого неполные отменяются.


static class SleepingCallable implements Callable<String> {

  final String name;
  final long period;

  SleepingCallable(final String name, final long period) {
    this.name = name;
    this.period = period;
  }

  public String call() {
    try {
      Thread.sleep(period);
    } catch (InterruptedException ex) { }
    return name;
  }
}

Теперь ниже я покажу, как invokeAll работает:

final ExecutorService pool = Executors.newFixedThreadPool(2);
final List<? extends Callable<String>> callables = Arrays.asList(
    new SleepingCallable("quick", 500),
    new SleepingCallable("slow", 5000));
try {
  for (final Future<String> future : pool.invokeAll(callables)) {
    System.out.println(future.get());
  }
} catch (ExecutionException | InterruptedException ex) { }
pool.shutdown();

Это дает следующий вывод:

C:\dev\scrap>java CompletionExample
... after 5 s ...
quick
slow

С помощью CompletionServiceмы видим другой вывод:

final ExecutorService pool = Executors.newFixedThreadPool(2);
final CompletionService<String> service = new ExecutorCompletionService<String>(pool);
final List<? extends Callable<String>> callables = Arrays.asList(
    new SleepingCallable("slow", 5000),
    new SleepingCallable("quick", 500));
for (final Callable<String> callable : callables) {
  service.submit(callable);
}
pool.shutdown();
try {
  while (!pool.isTerminated()) {
    final Future<String> future = service.take();
    System.out.println(future.get());
  }
} catch (ExecutionException | InterruptedException ex) { }

Это дает следующий вывод:

C:\dev\scrap>java CompletionExample
... after 500 ms ...
quick
... after 5 s ...
slow

Обратите внимание, что время относится к запуску программы, а не к предыдущему сообщению.


Вы можете найти полный код на обоих здесь.

Используя ExecutorCompletionService, вы можете получить немедленное уведомление, когда каждая из ваших работ завершается. В сравнении, ExecutorService.invokeAll(...) ожидает завершения всех ваших заданий, прежде чем вернуть коллекцию Futures:

// this waits until _all_ of the jobs complete
List<Future<Object>> futures = threadPool.invokeAll(...);

Вместо этого, когда вы используете ExecutorCompletionServiceвы сможете получать задания сразу после завершения каждого из них, что позволяет (например) отправлять их для обработки в другой пул потоков, регистрировать результаты и т. д.

ExecutorService threadPool = Executors.newFixedThreadPool(2);
ExecutorCompletionService<Result> compService
      = new ExecutorCompletionService<Result>(threadPool);
for (MyJob job : jobs) {
    compService.submit(job);
}
// shutdown the pool but the jobs submitted continue to run
threadPool.shutdown();
while (!threadPool.isTerminated()) {
    // the take() blocks until any of the jobs complete
    // this joins with the jobs in the order they _finish_
    Future<Result> future = compService.take();
    // this get() won't block
    Result result = future.get();
    // you can then put the result in some other thread pool or something
    // to immediately start processing it
    someOtherThreadPool.submit(new SomeNewJob(result));
}

Я на самом деле никогда не использовал ExecutorCompletionService, но я думаю, что случай, когда это может быть более полезным, чем "обычный" ExecutorService, был бы, когда вы хотите получить Фьючерсы завершенных задач в порядке завершения. С invokeAll вы просто получаете список, который может содержать сочетание незавершенных и выполненных задач в любой момент времени.

Сравнение с учетом только порядка результатов:

Когда мы используем CompletionService всякий раз, когда представленное задание заканчивается, результат помещается в очередь (Порядок выполнения). Тогда порядок представленных заданий и возвращаемых результатов уже не совпадает. Так что если вы беспокоитесь о порядке выполнения задач, используйте CompletionService

В то время как invokeAll возвращает список Фьючерсов, представляющих задачи, в том же последовательном порядке, что итератор для заданного списка задач, каждая из которых выполнена.

Другие вопросы по тегам