简体   繁体   English

java.util.concurrent.CompletableFuture中的异常传播

[英]Exception propagation in java.util.concurrent.CompletableFuture

There are two snippets of code. 有两个代码片段。

In the first one we create the CompletableFuture from the task which always throws some exception. 在第一个中,我们从任务中创建CompletableFuture,它始终抛出一些异常。 Then we apply "exceptionally" method to this future, then "theAccept" method. 然后我们将“特殊”方法应用于这个未来,然后是“theAccept”方法。 We DO NOT assign new future returned by theAccept method to any variable. 我们不会将接受方法返回的新未来分配给任何变量。 Then we invoke "join" on original future. 然后我们在原始未来上调用“加入”。 What we see is that "exceptionally" method has been invoked as well as the "thenAccept". 我们看到的是“异常”方法以及“thenAccept”被调用。 We see It because they printed appropriate lines in output. 我们看到它是因为它们在输出中打印了适当的线条 But the Exception has not been suppressed by "exceptionally" method. 但是,异常并未被“异常”方法所抑制。 Suppress exception and provide us with some default value instead is exactly what we expected from "exceptionally" in this case. 取消异常并为我们提供一些默认值,而这正是我们在这种情况下“异常”所期望的。

In second snippet we do almost the same but assign new returned future to variable and invoke "join" on It. 在第二个片段中,我们几乎完全相同,但将新返回的future分配给变量并在其上调用“join”。 In this case as expected exception is suppressed. 在这种情况下,预期的异常被抑制。

From my point of view for the first part the consistent behavior is either not to suppress exception and not to invoke "exceptionally" and "thenAccept" or call exceptionally and suppress exception. 从我的观点来看,第一部分的一致行为要么不抑制异常,要么不调用“exceptionally”和“thenAccept”或者异常调用并禁止异常。

Why do we have something in between? 为什么我们之间有什么东西?

First snippet: 第一个片段:

public class TestClass {
    public static void main(String[] args) {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(TestClass::doSomethingForInteger);

        future.exceptionally(e -> {
                    System.out.println("Exceptionally");
                    return 42;
                })
                .thenAccept(r -> {
                    System.out.println("Accept");
                });

        future.join();
    }

    private static int doSomethingForInteger() {
        throw new IllegalArgumentException("Error");
    }
}

Second snippet: 第二个片段:

public class TestClass {
    public static void main(String[] args) {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(TestClass::doSomethingForInteger);

        CompletableFuture<Void> voidCompletableFuture = future.exceptionally(e -> {
            System.out.println("Exceptionally");
            return 42;
        })
                .thenAccept(r -> {
                    System.out.println("Accept");
                });

        voidCompletableFuture.join();
    }

    private static int doSomethingForInteger() {
        throw new IllegalArgumentException("Error");
    }
}

There is no such thing as “suppressing an exception”. 没有“抑制异常”这样的事情。 When you invoke exceptionally , you are creating a new future, which will be completed with the result of the previous stage or the result of evaluating the function if the previous stage completed exceptionally. 当你exceptionally调用时,你正在创建一个新的未来,它将使用前一阶段的结果完成,或者如果前一阶段异常完成,则评估函数的结果。 The previous stage, ie the future you're invoking exceptionally on, is not affected. 前一阶段,即您正在exceptionally调用的未来,不会受到影响。

This applies to all methods chaining a depend function or action. 这适用于链接依赖函数或动作的所有方法。 Each of these methods creates a new future, which will be completed as documented. 这些方法中的每一种都创造了一个新的未来,将在记录后完成。 None of them affects the existing future you're invoking the method on. 它们都不会影响您正在调用该方法的现有未来。

Perhaps, it becomes much clearer with the following example: 也许,通过以下示例变得更加清晰:

CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    return "a string";
});

CompletableFuture<Integer> f2 = f1.thenApply(s -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2));
    return s.length();
});

f2.thenAccept(i -> System.out.println("result of f2 = "+i));

String s = f1.join();
System.out.println("result of f1 = "+s);

ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.DAYS);

Here, it should be clear that the result of the dependent stage, an Integer , can't supersede the result of the prerequisite stage, a String . 在这里,应该清楚的是,依赖阶段的结果,即Integer ,不能取代先决条件阶段的结果,即String These simply are two different futures with different results. 这些只是两种不同的未来,结果不同。 And since calling join() on f1 queries for the result of the first stage, it isn't dependent on f2 and hence, does not even wait for its completion. 并且因为在f1上调用join()查询第一阶段的结果,所以它不依赖于f2 ,因此甚至不等待它的完成。 (That's also the reason why the code waits for the end of all background activity at the end). (这也是代码在最后等待所有后台活动结束的原因)。

The usage of exceptionally is not different. exceptionally用法并没有什么不同。 It might be confusing that the next stage has the same type and even the same result in the non-exceptional case, but it doesn't change the fact that there are two distinct stages. 可能令人困惑的是,下一阶段在非例外情​​况下具有相同的类型甚至相同的结果,但它并没有改变存在两个不同阶段的事实。

static void report(String s, CompletableFuture<?> f) {
    f.whenComplete((i,t) -> {
        if(t != null) System.out.println(s+" completed exceptionally with "+t);
        else System.out.println(s+" completed with value "+i);
    });
}
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    throw new IllegalArgumentException("Error for testing");
});
CompletableFuture<Integer> f2 = f1.exceptionally(t -> 42);

report("f1", f1);
report("f2", f2);

ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.DAYS);

There seems to be a widespread mindset of the CompletableFuture chaining methods to be some kind of builder for a single future, which unfortunately is misleadingly wrong. 对于单一的未来, CompletableFuture链接方法似乎有一种广泛的思维方式可以成为某种形式的构建者,不幸的是,这种方法是错误的。 Another pitfall is the following mistake: 另一个陷阱是以下错误:

CompletableFuture<?> f = CompletableFuture.supplyAsync(() -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    System.out.println("initial stage");
    return "";
}).thenApply(s -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    System.out.println("second stage");
    return s;
}).thenApply(s -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    System.out.println("third stage");
    return s;
}).thenAccept(s -> {
    System.out.println("last stage");
});

f.cancel(true);
report("f", f);

ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.DAYS);

As explained, each chained method creates a new stage, so keeping a reference to the stage returned by the last chained method, ie the last stage, is suitable to get the final result. 如上所述,每个链接方法都会创建一个新阶段,因此保持对最后一个链接方法(即最后一个阶段)返回的阶段的引用适合于获得最终结果。 But canceling this stage will only cancel that last stage and none of the prerequisite stages. 但取消这个阶段只会取消最后阶段而不是前提阶段。 Also, after cancellation, the last stage does not depend on the other stages anymore, as it is already completed by cancellation and capable of reporting this exceptional result while the other, now unrelated stages are still being evaluated in the background. 此外,在取消之后,最后阶段不再依赖于其他阶段,因为它已经通过取消完成并且能够报告该异常结果,而其他现在不相关的阶段仍然在后台进行评估。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM