【问题标题】:CompletableFuture recursion for reliability可靠性的 CompletableFuture 递归
【发布时间】:2015-12-03 09:48:06
【问题描述】:

这是CompletableFuture 的非常基本的递归,我想做一个可靠的系统,所以每次都遇到异常重新启动进程,我相信它有太多问题并希望得到你的反馈

private CompletableFuture<?> recursion() {
return CompletableFuture.runAsync(() -> {
    //code here
    }).handleAsync((v, th) -> {
        if (th != null)
            return this.recursion();
        else
            return v;
    });
}

编辑1:

int tries =5;
private CompletableFuture<?> recursion() {
    return CompletableFuture.runAsync(() -> {
    //code here
    }).handleAsync((v, th) -> {
        if (th != null && tries-- > 0){
            Thread.sleep(1000);
            return this.recursion();
        }else
            return v;
    });
}

编辑2: 清理代码作为返回 CompletableFuture&lt;?&gt; 没有必要因此将其挂起以返回 void 考虑 @Holger 评论并使用 AtomicInteger 进行尝试

AtomicInteger tries =5;
private void recursion() {
    CompletableFuture.runAsync(() -> {
    //code here
    }).whenCompleteAsync((v, th) -> {
        if (th != null && ( tries.getAndDecrement() > 0 ) ){
            Thread.sleep(1000);
            this.recursion();
        });
}

请给我反馈,我在争论,但真的很感激。

【问题讨论】:

  • 您还应该考虑到使用递归可能会因StackOverflowError 或无限循环而崩溃。
  • 是的,我认为它实际上是在尝试多次尝试并在每次尝试之间休眠。
  • 这样的东西已经存在,但我建议你看看rxjava。还有asyn-cretry很好:github.com/nurkiewicz/async-retry
  • 我正在尝试使用纯 java 来实现,我已经在 github.com/bassemZohdy/simple-reactive-streams 处实现了反应流,但我想用 java CompletableFuture 来实现
  • 我从一开始就明白了,但让我问一下它的副作用是什么,而不是让泛型类型签名有意义,这是相当重要的一点。

标签: java recursion functional-programming java-8 future


【解决方案1】:

一般情况下,发生异常时简单地重试操作,而不处理异常来分析失败原因,离创建可靠的系统还很远。

但是,如果您想实现重试,您的代码将无法正确执行此操作。

您的代码恰好被编译器接受,因为您使用了一个不会产生值并返回 CompletableFuture&lt;?&gt; 的操作。这隐藏了您的代码的问题:

传递给handleAsync 的双函数应该提供结果值,但您调用this.recursion() 会产生CompletableFuture&lt;?&gt;。编译器不介意在非异常情况下返回v,因为VoidCompletableFuture&lt;?&gt; 有一个共同的超类型Object,所以整个方法实际上是返回一个CompletableFuture&lt;Object&gt;兼容返回类型CompletableFuture&lt;?&gt;

如果您将返回类型声明为CompletableFuture&lt;Void&gt;,则可以立即识别出逻辑错误:在特殊情况下,您正在启动另一个异步操作,但由于您没有检查它的结果而只是返回一个CompletableFuture&lt;?&gt;,然后被视为Object,调用者将永远不会注意到重试(或后续重试)是否失败。调用者将始终收到一个报告成功的CompletableFuture&lt;?&gt;,并将(Void)nullCompletableFuture&lt;?&gt; 作为结果值。

一般来说,你不应该使用递归来进行重复。没有理由这样做。让我们用一个返回值的动作来演示逻辑:

CompletableFuture<String> performAsyncAction() {
    Supplier<String> action=() -> {
        if(Math.random()>0.2)
            throw new IllegalStateException("simulated failure");
        return "value implying success";
    };
    int retries=5;
    return CompletableFuture.supplyAsync(() -> {
        try { return action.get(); }
        catch(Throwable t) {
            for(int i=0; i<retries; i++) try {
                Thread.sleep(1000);
                return action.get();
            } catch(Throwable next) { t.addSuppressed(next); }
            throw t;
        }
    });
}

很容易适应使用RunnablerunAsyncCompletableFuture&lt;Void&gt;


更新:如果您只想安排重试而不向发起者提供反馈,您可以通过等待延迟过去来实现它而不阻塞线程:

static ScheduledExecutorService e = Executors.newSingleThreadScheduledExecutor();

static void performAsyncAction(Runnable r, int tries, long delay, TimeUnit u) {
    if(tries>0)
        e.execute(()-> { try { r.run(); } catch(Throwable t) {
            e.schedule(()->performAsyncAction(r, tries-1, delay, u), delay, u);
        }});
}

这使用递归,因为它解决了 lambda 表达式。如果您使用内部类,同样可以在没有递归的情况下工作:

static ScheduledExecutorService e = Executors.newSingleThreadScheduledExecutor();

static void performAsyncAction(Runnable r, int tries, long delay, TimeUnit u) {
    if(tries>0)
        e.execute(new Runnable() {
            int left = tries;
            public void run() {
                try { r.run(); } catch(Throwable t) {
                    if(--left > 0) e.schedule(this, delay, u);
                }
            }
        });
}

【讨论】:

  • 你的代码意味着线程将被锁定在 for 循环中进行所有尝试,我不想这样做,我理解你的观点,但正如我所说的那样,我想得到来自用户要重试的异常。
  • 循环在异步操作中运行,即不同的线程。将迭代分离到单独的线程中是没有意义的,这些线程将在每个 handleAsync 创建一个 dependent 操作时相互等待。事实上,底层实现可能足够聪明,可以识别出各个阶段相互依赖,并将前一个阶段的线程重用于下一个阶段。但如果没有,您将创建多达retries 数量的附加线程,但没有任何好处。您无法更改“如果前一次尝试失败,请重试”的顺序性质。
  • 等待 handleAsync ??不是它没有等待,因为它正在释放第一个线程,在我的情况下,当使用线程池运行它时,实际上线程被相同的操作释放和重用,但是使用 for 循环根本不会释放线程,换句话说,如果你的 CompletableFuture没有完成并不意味着资源没有被释放。
  • @Bassem Reda Zohdy:讨论的意义何在?除了你的原始代码无论如何都不起作用之外,它会在最好的情况下释放并重用一个线程,而我的解决方案将在整个操作过程中使用一个线程。区别在哪里?这意味着在这两种情况下,整个操作都使用一个线程。但是您的尝试使它变得更加复杂,并且不能保证一个阶段将使用前一个刚刚发布的同一线程......
  • 第一次我的代码正在工作并且做我想要它做的检查这个要点以获取更多细节gist.github.com/bassemZohdy/f100789df84f7fa7a77c,这个讨论的重点是让它变得更好并得到其他人可以告诉我的问题,不同之处在于您的解决方案线程一直被锁定并且资源未释放告诉所有尝试都已完成,包括 5 次 Thread.sleep,是的,您正在使用一个线程但一直阻塞它,包括睡眠时间在我的情况下它被释放并且重复使用,睡眠时间结束。
猜你喜欢
  • 1970-01-01
  • 2021-07-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-27
  • 2013-03-27
  • 1970-01-01
相关资源
最近更新 更多