英文:
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<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);
// 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<User> user = service.findUser("foo");
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) -> 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
注解元素的两个指定代理细节的地方看出:mode
和 proxyTargetClass
。
问题示例
最后,将这应用于问题示例将使其具体化。调用 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论