简体   繁体   中英

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

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.

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 .

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 eg 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: eg 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 eg CompletableFuture.get() ), the value it will get from that Future will be the same results value passed to CompletableFuture.completedFuture in the GitHubLookupService code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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