我想实现一些可以获得工作者(callables)集合的东西,在线程池上并行运行它,当最快的worker返回结果时,优雅地关闭(ExecutorService.shutdownNow)其他工作者,以便不再浪费资源。如果所有工作者都以异常结束,我需要重新抛出最重要的工作者(工人抛出的所有异常都与importance
值相关联)。此外,我需要对整个执行程序进行超时,如果它们运行的时间太长,将终止所有工作程序。
我已经考虑过使用RxJava了,因为这样可以在这里实现简洁明了的解决方案。但也许你可以为它提出一些更好的工具(CompletableFutures,ForkJoinTasks?)。这是我已经编写过的代码,但它远不是一个有效的解决方案(我对反应性编程并没有真正的经验,因此真的很挣扎):
public T run(Collection<? extends Worker<T>> workers, long timeout) {
ExecutorService executorService = Executors.newFixedThreadPool(workers.size());
return Observable.from(workers)
.timeout(timeout, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.from(executorService))
.map(worker -> {
try {
T res = worker.call();
executorService.shutdownNow();
return res;
} catch (Exception e) {
throw Exceptions.propagate(e);
}
}).doOnError(Exceptions::propagate).toBlocking().first();
如果有任何帮助,我将不胜感激。
答案 0 :(得分:2)
相当有趣的技术挑战,谢谢你的提问。以下是使用CompletableFuture
进行Java8的解决方案。在Java7中,您可以以完全相同的方式使用io.netty.util.concurrent.Promise
。
最简单的部分是处理正常情况:
更棘手的部分是在每个单独抛出保持相同的逻辑流时异常完成。这可以通过累积所有异常并在计数达到最后一个失败作业中的作业计数时异常完成未来来解决。传递的异常是按排名排序的列表中的第一个(这里它将是最小排名,相应地更改)。调用future.get()
并将其包含在ExecutionException
。
最后,因为您将来会回来,您可以将超时值传递给get
方法。
所以这里是实际的工作解决方案,异常类和测试如下:
public <R> CompletableFuture<R> execute(Collection<? extends Callable<R>> jobs) {
final CompletableFuture<R> result = new CompletableFuture<>();
if (jobs == null || jobs.isEmpty()) {
result.completeExceptionally(new IllegalArgumentException("there must be at least one job"));
return result;
}
final ExecutorService service = Executors.newFixedThreadPool(jobs.size());
// accumulate all exceptions to rank later (only if all throw)
final List<RankedException> exceptions = Collections.synchronizedList(Lists.newArrayList());
final AtomicBoolean done = new AtomicBoolean(false);
for (Callable<R> job: jobs) {
service.execute(() -> {
try {
// this is where the actual work is done
R res = job.call();
// set result if still unset
if (done.compareAndSet(false, true)) {
// complete the future, move to service shutdown
result.complete(res);
}
// beware of catching Exception, change to your own checked type
} catch (Exception ex) {
if (ex instanceof RankedException) {
exceptions.add((RankedException) ex);
} else {
exceptions.add(new RankedException(ex));
}
if (exceptions.size() >= jobs.size()) {
// the last to throw and only if all have thrown will run:
Collections.sort(exceptions, (left, right) -> Integer.compare(left.rank, right.rank));
// complete the future, move to service shutdown
result.completeExceptionally(exceptions.get(0));
}
}
});
}
// shutdown also on error, do not wait for this stage
result.whenCompleteAsync((action, t) -> service.shutdownNow());
return result;
}
RankedExeption
:
public static class RankedException extends Exception {
private final int rank;
public RankedException(Throwable t) {
this(0, t);
}
public RankedException(int rank, Throwable t) {
super(t);
this.rank = rank;
}
}
现在有两个测试,成功和失败的情况(有点简化,但仍然):
@Rule
public ExpectedException exception = ExpectedException.none();
private static class TestJob implements Callable<Double> {
private final int index;
private final int failOnCount;
TestJob(int index, int failOnCount) {
this.index = index;
this.failOnCount = failOnCount;
}
@Override
public Double call() throws RankedException {
double res = 0;
int count = (int) (Math.random() * 1e6) + 1;
if (count > failOnCount) {
throw new RankedException(count, new RuntimeException("job " + index + " failed"));
}
for (int i = 0; i < count; i++) {
res += Math.random();
}
return res;
}
}
@Test
public void test_success() throws Exception {
List<TestJob> jobs = Lists.newArrayList();
for (int i = 0; i < 10; i++) {
jobs.add(new TestJob(i, (int)(5*1e5))); // 50% should be alright
}
CompletableFuture<Double> res = execute(jobs);
logger.info("SUCCESS-TEST completed with " + res.get(30, TimeUnit.SECONDS));
}
@Test
public void test_failure() throws Exception {
List<TestJob> jobs = Lists.newArrayList();
for (int i = 0; i < 10; i++) {
jobs.add(new TestJob(i, 0)); // all should fail
}
CompletableFuture<Double> res = execute(jobs);
exception.expect(ExecutionException.class);
try {
res.get(30, TimeUnit.SECONDS);
} catch (ExecutionException ex) {
logger.severe(String.format("FAIL-TEST rank: %s", ((RankedException) ex.getCause()).rank));
throw ex;
}
}
最后,测试的截断输出运行:
INFO:SUCCESS-TEST已完成115863.20802680103
SEVERE:FAIL-TEST排名:388150
处理完成,退出代码为0
注意:您可能希望通过AtomicBoolean
进一步发送信号,以便在第一次准备就绪时实际发出所有线程的信号
我不保证上面的代码没有错误,因为它是在匆忙中完成的,测试是初步的。它旨在表明进一步挖掘的方向。
答案 1 :(得分:2)
RxJava的完美应用。要获得并行操作,请在flatMap
内使用subscribeOn
和flatMap
。要使用materialize
获取错误,请在成功返回值后立即停止使用takeUntil
。使用timeout
运算符来满足超时要求。
ExecutorService executorService =
Executors.newFixedThreadPool(workers.size());
Scheduler scheduler = Schedulers.from(executorService);
return Observable
.from(workers)
.flatMap(worker ->
Observable.fromCallable(worker)
.subscribeOn(scheduler)
.materialize())
.takeUntil(notification -> notification.hasValue())
.toList()
.timeout(30, TimeUnit.SECONDS)
.flatMap(
list -> {
Notification<T> last = list.get(list.size() - 1);
if (last.hasValue())
return Observable.just(last.getValue());
else {
// TODO get the error notification from the list
// with the highest importance and emit
return Observable.<T>error(err);
}
}).subscribe(subscriber);
答案 2 :(得分:0)
您是否查看了RxJava AMB运算符?但是,您需要验证它是否在第一个onComplete上完成,因为文档没有说明任何内容。