Java 8 CompletableFuture中具有默认值的超时

Java 8 CompletableFuture中具有默认值的超时,java,java-8,Java,Java 8,假设我有一些异步计算,例如: CompletableFuture .supplyAsync(() -> createFoo()) .thenAccept(foo -> doStuffWithFoo(foo)); 如果异步供应商根据某个指定的超时超时超时,是否有一种很好的方法为foo提供默认值?理想情况下,这种功能也会试图取消运行缓慢的供应商。例如,是否存在与以下假设代码类似的标准库功能: CompletableFuture .supp

假设我有一些异步计算,例如:

CompletableFuture
        .supplyAsync(() -> createFoo())
        .thenAccept(foo -> doStuffWithFoo(foo));
如果异步供应商根据某个指定的超时超时超时,是否有一种很好的方法为foo提供默认值?理想情况下,这种功能也会试图取消运行缓慢的供应商。例如,是否存在与以下假设代码类似的标准库功能:

CompletableFuture
        .supplyAsync(() -> createFoo())
        .acceptEither(
                CompletableFuture.completedAfter(50, TimeUnit.MILLISECONDS, DEFAULT_FOO),
                foo -> doStuffWithFoo(foo));
或者更好:

CompletableFuture
        .supplyAsync(() -> createFoo())
        .withDefault(DEFAULT_FOO, 50, TimeUnit.MILLISECONDS)
        .thenAccept(foo -> doStuffWithFoo(foo));
我知道
get(timeout,unit)
,但我想知道是否有一种更好的标准方法可以像上面代码中建议的那样,以异步和反应方式应用超时

编辑:这里有一个灵感来源于的解决方案,但不幸的是它阻塞了一个线程。如果我们依靠createFoo()异步检查超时并抛出它自己的超时异常,它将在不阻塞线程的情况下工作,但会给供应商的创建者带来更多负担,并且仍然会有创建异常的成本(如果没有“快速抛出”,这可能会很昂贵)

静态供应商包装(可调用){
返回()->{
试一试{
返回callable.call();
}捕获(运行时异常e1){
抛出e1;
}捕获(可丢弃的e2){
抛出新的运行时异常(e2);
}
};
}
完全未来
.supplyAsync(wrapped(()->CompletableFuture.supplyAsync(()->createFoo()).get(50,TimeUnit.ms)))
.例外情况(e->“违约”)
。然后接受同步(s->doStuffWithFoo(foo));

我认为在提供默认值时,您总是需要额外的线程监控。我可能会使用两个SupplySync调用,默认调用包装在实用程序API中,由Acceptor链接。如果您希望包装您的供应商,那么您可以使用实用程序API为您发出“任一”调用:

public class TimeoutDefault {
    public static <T> CompletableFuture<T> with(T t, int ms) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(ms);
            } catch (InterruptedException e) { }
            return t;
        });
    }

    public static <T> Supplier<T> with(Supplier<T> supplier, T t, int ms) {
        return () -> CompletableFuture.supplyAsync(supplier)
            .applyToEither(TimeoutDefault.with(t, ms), i -> i).join();
    }
}

CompletableFuture<Void> future = CompletableFuture
        .supplyAsync(Example::createFoo)
        .acceptEither(
            TimeoutDefault.with("default", 1000),
            Example::doStuffWithFoo);

CompletableFuture<Void> future = CompletableFuture
        .supplyAsync(TimeoutDefault.with(Example::createFoo, "default", 1000))
        .thenAccept(Example::doStuffWithFoo);
公共类超时默认值{
公共静态完全未来(T,int-ms){
返回CompletableFuture.SupplySync(()->{
试一试{
睡眠(ms);
}捕获(中断异常e){}
返回t;
});
}
公共静态供应商(供应商供应商、T T、int ms){
return()->CompletableFuture.supplyAsync(供应商)
.applytoother(TimeoutDefault.with(t,ms),i->i.join();
}
}
CompletableFuture=CompletableFuture
.SupplySync(示例::createFoo)
.接受(
TimeoutDefault.with(“default”,1000),
例如:dostufwithfoo);
CompletableFuture=CompletableFuture
.supplyAsync(TimeoutDefault.with(示例::createFoo,“default”,1000))
.thenAccept(示例::dostufwithfoo);

CompletableFuture.SupplySync只是一个帮助器方法,它为您创建一个CompletableFuture,并将任务提交到ForkJoin池

您可以创建自己的SupplySync以满足您的需求,如下所示:

private static final ScheduledExecutorService schedulerExecutor = 
                                 Executors.newScheduledThreadPool(10);
private static final ExecutorService executorService = 
                                 Executors.newCachedThreadPool();


public static <T> CompletableFuture<T> supplyAsync(
        final Supplier<T> supplier, long timeoutValue, TimeUnit timeUnit,
        T defaultValue) {

    final CompletableFuture<T> cf = new CompletableFuture<T>();

    // as pointed out by Peti, the ForkJoinPool.commonPool() delivers a 
    // ForkJoinTask implementation of Future, that doesn't interrupt when cancelling
    // Using Executors.newCachedThreadPool instead in the example
    // submit task
    Future<?> future = executorService.submit(() -> {
        try {
            cf.complete(supplier.get());
        } catch (Throwable ex) {
            cf.completeExceptionally(ex);
        }
    });

    //schedule watcher
    schedulerExecutor.schedule(() -> {
        if (!cf.isDone()) {
            cf.complete(defaultValue);
            future.cancel(true);
        }

    }, timeoutValue, timeUnit);

    return cf;
}

DZone有一篇关于如何解决这个问题的好文章:

我不确定代码的版权,因此我不能在这里复制它。该解决方案与Dane White的解决方案非常相似,但它使用了一个线程池,其中包含一个线程加上
schedule()
,以避免浪费一个线程来等待超时


它还会抛出一个
TimeoutException
,而不是返回默认值。

没有标准的库方法来构造一个在超时后提供值的CompletableFuture。也就是说,以最小的资源开销推出自己的产品非常简单:

private static final ScheduledExecutorService EXECUTOR
        = Executors.newSingleThreadScheduledExecutor();

public static <T> CompletableFuture<T> delayedValue(final T value,
                                                    final Duration delay) {
    final CompletableFuture<T> result = new CompletableFuture<>();
    EXECUTOR.schedule(() -> result.complete(value),
                      delay.toMillis(), TimeUnit.MILLISECONDS);
    return result;
}
如果远程客户端调用异常完成(例如,
SocketTimeoutException
),我们可以快速失败并立即使用缓存值

CompletableFuture.anyOf(CompletableFuture…
可以与此
delayedValue
原语组合使用上述语义包装
CompletableFuture

@SuppressWarnings("unchecked")
public static <T> CompletableFuture<T> withDefault(final CompletableFuture<T> cf,
                                                   final T defaultValue,
                                                   final Duration timeout) {
    return (CompletableFuture<T>) CompletableFuture.anyOf(
        cf.exceptionally(ignoredException -> defaultValue),
        delayedValue(defaultValue, timeout));
}
CompletableFuture
s更准确地称为承诺,因为它们将
Future
的创建与其完成分离。确保使用专用线程池进行繁重的CPU工作。要为昂贵的计算创建
CompletableFuture
,应使用
CompletableFuture\supplyAsync(供应商、执行者)
重载,因为
\supplyAsync(供应商)
重载默认为公共
ForkJoinPool
。返回的
CompletableFuture
无法取消其任务,因为
Executor
接口未公开此功能。更一般地说,依赖的
CompletableFuture
s不会取消他们的父母,例如
cf.thenply(f)。cancel(true)
不会取消
cf
。如果您需要
ExecutorService
s返回的
未来
s功能,我建议您坚持使用该功能。

在Java 9中,将有,它满足您的需要,尽管它不会取消缓慢的供应商


还有一个线程,它在超时情况下异常完成。

我想知道线程.sleep(2000);在您的示例中实际上被打断了。事实并非如此。如果我将您的示例从
ForkJoinPool.commonPool().submit
更改为
Executors.newFixedThreadPool(1).submit
,那么它就是。我想知道为什么…你是对的@Peti!ForkJoinPool中的commonPool提供类型为
ForkJoinTask
的Future实现,在取消时不会中断Future,而执行者提供类型为
FutureTask
的Future实现。请参见
ForkJoinTask
cancel
方法中的Javadocs:maybruptfrunning:此值在默认实现中无效,因为中断不用于控制取消。每次调用都会创建一个新线程。从性能的角度来看,这绝对是荒谬的。@Ruben是否有理由将工作分为cachedPool和scheduled pool?也就是说,您能否删除
执行器服务
未来
    a = supplyAsync(() -> {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e1) {
            // ignore
        }
        return "hi";
    }, 1, TimeUnit.SECONDS, "default");
private static final ScheduledExecutorService EXECUTOR
        = Executors.newSingleThreadScheduledExecutor();

public static <T> CompletableFuture<T> delayedValue(final T value,
                                                    final Duration delay) {
    final CompletableFuture<T> result = new CompletableFuture<>();
    EXECUTOR.schedule(() -> result.complete(value),
                      delay.toMillis(), TimeUnit.MILLISECONDS);
    return result;
}
interface RemoteServiceClient {
    CompletableFuture<Foo> getFoo();
}

final RemoteServiceClient client = /* ... */;
final Foo cachedFoo = /* ... */;
final Duration timeout = /* ... */;

client.getFoos()
    .exceptionally(ignoredException -> cachedFoo)
    .acceptEither(delayedValue(cachedFoo, timeout),
        foo -> /* do something with foo */)
    .join();
@SuppressWarnings("unchecked")
public static <T> CompletableFuture<T> withDefault(final CompletableFuture<T> cf,
                                                   final T defaultValue,
                                                   final Duration timeout) {
    return (CompletableFuture<T>) CompletableFuture.anyOf(
        cf.exceptionally(ignoredException -> defaultValue),
        delayedValue(defaultValue, timeout));
}
withDefault(client.getFoos(), cachedFoo, timeout)
    .thenAccept(foo -> /* do something with foo */)
    .join();