Spring Boot中的@Async方法实际上是异步/非阻塞的吗?

huangapple go评论62阅读模式
英文:

How are Spring boot @Async methods actually async/non-blocking?

问题

以下示例摘自Spring 'Getting Started Creating Asynchronous Methods

@Service
public class GitHubLookupService {

  @Async
  public CompletableFuture<User> findUser(String user) throws InterruptedException {
    logger.info("Looking up " + user);
    String url = String.format("https://api.github.com/users/%s", user);
    User results = restTemplate.getForObject(url, User.class);
    // 为演示目的人工延迟1秒
    Thread.sleep(1000L);
    return CompletableFuture.completedFuture(results);
  }

}

据我所知,关于计算机科学中的异步方法,它应立即返回且应为非阻塞的。

所以假设在Spring的某处,比如我的代码findUser()被这样调用:

CompletableFuture<User> user = service.findUser("foo");

实际上,这会阻塞。它会阻塞不同的线程在Executor服务上,但由于Thread.sleep(1000L),它会被阻塞。 对吗?

那么这是如何异步的呢?

我的意思是,CompletableFuture的整个目的是获得对将来完成的计算的引用。但是在这里,当我返回完成的future时,计算已经结束了,也就是说,我们正在使用 CompletableFuture.completedFuture(results)

那么在这种情况下使用CompletableFuture的意义何在?我的意思是,如果我要在计算结束并获得结果时才返回并阻塞,我倒不如只返回结果,而不是 CompletableFuture

这样真正的非阻塞/异步在哪里?

我在哪里错了?我漏掉了什么?

谢谢。

英文:

The following example is taken from Spring' Getting Started Creating Asynchronous Methods.

@Service
public class GitHubLookupService {

  @Async
  public CompletableFuture&lt;User&gt; findUser(String user) throws InterruptedException {
    logger.info(&quot;Looking up &quot; + user);
    String url = String.format(&quot;https://api.github.com/users/%s&quot;, user);
    User results = restTemplate.getForObject(url, User.class);
    // Artificial delay of 1s for demonstration purposes
    Thread.sleep(1000L);
    return CompletableFuture.completedFuture(results);
  }

}

AFAIK my knowledge of async methods in computer science goes - It should return immediately. It should be non-blocking.

So lets say somewhere in Spring lets say my code findUser() is called like so:

CompletableFuture&lt;User&gt; user = service.findUser(&quot;foo&quot;);

This would actually block. It would block a different thread on the Executor service but it would block due to the Thread.sleep(1000L). Correct ?

So how is this async ?

I mean the whole point of CompletableFuture is to get a reference to a computation that will be completed in the future. But here when I get back the completed future the computation is already over, ie. we are using CompletableFuture.completedFuture(results).

So what is the point of having a CompletableFuture in this case ? I mean if I am going to block and return only when my computation is over and I have the results, I might as well just return the result and not the CompletableFuture.

How is this truly non-blocking/async ?

The only non-blocking aspect I find here is offload to a different thread, nothing else.

Am I going wrong somewhere ? What am I missing ?

Thanks.

答案1

得分: 1

问题出在你创建Future的方式上。你使用的代码是:

CompletableFuture.completedFuture(results)

根据JavaDoc中的描述,这只是在同步和异步之间的包装器,计算是同步完成的:

返回一个已经以给定值完成的新CompletableFuture

在某些情况下,这是很有用的,只有在某些输入情况下才希望进行异步工作。考虑以下示例:

(x) -> x==0 ? CompletableFuture.completedFuture(0) : CompletableFuture.supplyAsync(expensiveComputation)

我希望这能清楚地展示区别 - 如果你想要真正的异步计算,你需要使用supplyAsync函数:

返回一个新的CompletableFuture,通过在ForkJoinPool.commonPool()中运行的任务异步完成,该任务通过调用给定的Supplier来获得值。

英文:

The problem lies in the way you create your Future. The code you use is

CompletableFuture.completedFuture(results)

Quoting from the JavaDoc, this is only a wrapper between sync and async, where the computation was done synchronously:

> Returns a new CompletableFuture that is already completed with the given value.

This is useful in certain situations where you only want to do asynchronous work for some inputs. Consider

(x) -&gt; x==0 ? CompletableFuture.completedFuture(0) : CompletableFuture.supplyAsync(expensiveComputation)

I hope this makes the difference clear - if you want truly async computations, you need to use the supplyAsync function:

> Returns a new CompletableFuture that is asynchronously completed by a task running in the ForkJoinPool.commonPool() with the value obtained by calling the given Supplier.

答案2

得分: 1

你所遗漏的细节是,当使用 @Async(并且配置正确)时,将使用代理bean来包装你的服务bean。通过代理对异步方法的任何调用都将使用 Spring TaskExecutor 来异步运行该方法。

将方法响应包装在同步的 Future 中,比如 CompletableFuture.completedFuture,这是必要的,以便返回类型可以是 Future。然而,你返回的 Future 并不是代理返回的 Future。相反,代理返回由 TaskExecutor 提供的 Future,该 Future 将以异步方式处理。通过例如 CompletableFuture.completedFuture 创建的 Future 将被代理解包,并且其完成结果将由代理的 Future 返回。

代理文档

我在 Spring 参考文档@Async@EnableAsync Javadoc 中没有明确说明所有上述代理细节。然而,可以通过阅读所提供内容之间的隐含信息来拼凑出这些细节。

@Async Javadoc 在提及服务代理时简要提到,并解释了为什么在服务方法的实现中使用 CompletableFuture.completedFuture

> 代理返回的 Future 句柄将是一个实际的异步 Future,可用于跟踪异步方法执行的结果。然而,由于目标方法需要实现相同的签名,它必须返回一个临时的 Future 句柄,只需通过一个值:例如 Spring 的 AsyncResult,EJB 3.1 的 AsyncResult,或 CompletableFuture.completedFuture(Object)

代理涉及的事实还可从 @EnableAsync 注解元素的两个指定代理细节的地方看出:modeproxyTargetClass

问题示例

最后,将这应用于问题示例将使其具体化。调用 GitHubLookupService bean 上的 findUser 方法的代码实际上将调用代理类上的方法,而不是直接调用 GitHubLookupService 实例上的方法。代理类的 findUser 方法将任务提交给 Spring 的 TaskExecutor,并返回一个 CompletableFuture,该 CompletableFuture 将在提交的任务完成时异步完成。

提交的任务将调用非代理的 GitHubLookupService 中的实际 findUser 方法。这将执行 REST 调用,休眠 1 秒,并返回一个带有 REST 结果的已完成 CompletableFuture

由于这个任务是在由 Spring 的 TaskExecutor 创建的单独线程中进行的,调用代码将立即继续执行 GitHubLookupService.findUser 调用之后的代码,尽管它至少需要 1 秒才能返回。

如果调用代码中使用 findUser 调用的结果(使用例如 CompletableFuture.get())将从 Future 获取的值,将是传递给 GitHubLookupService 代码中的 CompletableFuture.completedFuture 的相同 results 值。

英文:

The detail you're missing is that when @Async is used (and correctly configured), a proxy bean will be used, wrapping your service bean. Any calls to the asynchronous methods through the proxy will use a Spring TaskExecutor to asynchronously run the method.

Wrapping the method response in a synchronous Future such as CompletableFuture.completedFuture is necessary so the return type can be a Future. However, the Future you return is not the one returned by the proxy. Rather, the proxy returns a Future provided by the TaskExecutor, which will be processed asynchronously. The Future you create through e.g. CompletableFuture.completedFuture is unwrapped by the proxy, and its completion result is returned by the proxy's Future.

Proxying documentation

I don't see all of the aforementioned proxying details explicitly stated in either the Spring reference documentation or in the @Async or @EnableAsync Javadocs. However, the details can be pieced together by reading between the lines of what is provided.

The @Async Javadocs mentions the service proxy in passing, and explains why CompletableFuture.completedFuture is used in the service method's implementation:

> A Future handle returned from the proxy will be an actual asynchronous Future that can be used to track the result of the asynchronous method execution. However, since the target method needs to implement the same signature, it will have to return a temporary Future handle that just passes a value through: e.g. Spring's AsyncResult, EJB 3.1's AsyncResult, or CompletableFuture.completedFuture(Object).

The fact that proxying is involved is also made apparent by the fact that two of the @EnableAsync annotation elements specify proxying details: mode and proxyTargetClass.

Question example

Finally, applying this to the question example will make it concrete. Code that calls the findUser method on the GitHubLookupService bean will actually be calling a method on a proxy class, rather than directly on the GitHubLookupService instance. The proxy class's findUser method submits a task to Spring's TaskExecutor, and return a CompletableFuture that will asynchronously be completed when the submitted task completes.

The submitted task will call the actual findUser method in the non-proxied GitHubLookupService. This will will perform the REST call, sleep 1 second, and return a completed CompletableFuture with the REST results.

Since this task is happening in a separate thread created by Spring's TaskExecutor, the calling code will continue to proceed past the GitHubLookupService.findUser call immediately, even though it will take at least 1 second for it to return.

If the result of the findUser call is used in the calling code (using e.g. CompletableFuture.get()), the value it will get from that Future will be the same results value passed to CompletableFuture.completedFuture in the GitHubLookupService code.

huangapple
  • 本文由 发表于 2020年7月26日 19:27:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/63099493.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定