ExecutorCompletionService? Зачем нужен один, если у нас есть invokeAll?
Если мы используем ExecutorCompletionService, мы можем представить ряд задач в виде Callable
и получить результат, взаимодействуя с CompletionService
как queue
,
Но есть и invokeAll
из ExecutorService
который принимает Collection
задач, и мы получаем список Future
чтобы получить результаты.
Насколько я могу судить, нет никакой пользы в использовании одного или другого (за исключением того, что мы избегаем for
цикл с помощью invokeAll
что нам придется submit
задачи к CompletionService
) и по сути это одна и та же идея с небольшим отличием.
Итак, почему есть 2 различных способа представить серию задач? Правильно ли я понимаю, что производительность эквивалентна? Есть ли случай, когда один из них более подходит, чем другой? Я не могу думать об одном.
4 ответа
Используя ExecutorCompletionService.poll/take
Вы получаете Future
s как они заканчивают, в порядке завершения (более или менее). С помощью 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(...)
ожидает завершения всех ваших заданий, прежде чем вернуть коллекцию Future
s:
// 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
возвращает список Фьючерсов, представляющих задачи, в том же последовательном порядке, что итератор для заданного списка задач, каждая из которых выполнена.