异步处理结果
英文原文
https://playframework.com/documentation/2.6.x/JavaAsync
创建异步的controller
Play是一个自底向上的异步框架,play处理所有的request都是异步、非阻塞的。
默认的方式是使用异步的controller。换句话说,contrller中的应用代码需要避免阻塞,i.e.不能等待某一个操作。场景的阻塞操作有JDBC调用、streaming API、HTTP请求和耗时计算。
虽然可以通过增加线程池中线程的数量来让阻塞的controller处理更多的请求。还是通过以下建议使controller异步来保持程序的可靠性和可扩展性。
创建非阻塞的action
在Play的机制中,action需要尽可能的块,非阻塞。在计算未完成时,可以返回promise of a result!
Java8提供了一个新的APICompletionStage
。一个CompletionStage<Result>
实例最终会返回一个Result类型的对象。通过使用CompletionStage<Result>
代替普通的Result
,action可以非阻塞的返回,Play最终会处理返回的结果。
如何创建CompletionStage
在创建CompletionStage<Result>
前,我们需要另外一项保证:保证会给到我们计算中实际需要的值。
CompletionStage<Double> promiseOfPIValue = computePIAsynchronously();
// Runs in same thread
CompletionStage<Result> promiseOfResult = promiseOfPIValue.thenApply(pi ->
ok("PI value computed: " + pi)
);
Play的异步API会返回一个CompletionStage实例。比如通过play.libs.WS调用外部web服务,或者使用Akka进行异步计算。
这个例子中,CompletionStage.thenApply
在进行当前阶段的计算时与之前的任务在同一个线程中执行。This is fine when you have a small amount of CPU bound logic with no blocking.
若要使用异步计算,可以调用CompletionStage.supplyAsync()
方法
// import static java.util.concurrent.CompletableFuture.supplyAsync;
// creates new task
CompletionStage<Integer> promiseOfInt = CompletableFuture.supplyAsync(() ->
intensiveComputation());
使用supplyAsync创建的新任务会被安置在fork join pool中,而且可能会被不同的线程调用,虽然这里使用的是默认的执行器,也可以指定一个执行器。
注意:只有*Async结尾的方法才是异步执行的 |
使用HttpExecutionContext
在Action中使用Java CompletionStage
就必须显示地为HTTP执行环境指明一个executor,来保证HTTP.Context
保存在当前域中。如果没有提供http执行环境,在调用request()
等依赖HTTP.Context
的方法时会得到一个”There is no HTTP Context available from here”的错误。
可以通过注入的方式提供一个play.libs.concurrent.HttpExecutionContext
的实例。
import play.libs.concurrent.HttpExecutionContext;
import play.mvc.*;
import javax.inject.Inject;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
public class MyController extends Controller {
private HttpExecutionContext httpExecutionContext;
@Inject
public MyController(HttpExecutionContext ec) {
this.httpExecutionContext = ec;
}
public CompletionStage<Result> index() {
// Use a different task with explicit EC
return calculateResponse().thenApplyAsync(answer -> {
// uses Http.Context
ctx().flash().put("info", "Response updated!");
return ok("answer was " + answer);
}, httpExecutionContext.current());
}
private static CompletionStage<String> calculateResponse() {
return CompletableFuture.completedFuture("42");
}
}
请在Java thread locals获取更多Java thread locals与HttpExecutionContext的细节。
使用CustomExecutionContext和HttpExecution
使用CompletionStage或者HttpExecutionContext才仅仅完成了一半。目前仍然在使用Play默认的执行上下文,如果调用了一个像JDBC这样的阻塞API,我们需要让当前的ExecutionStage移出Play的线程池,在一个不同的executor中执行。可以通过创建一个拥有 custom dispatcher的play.libs.concurrent.CustomExecutionContext
子类来实现。
定义一个执行上下文
public class MyExecutionContext extends CustomExecutionContext {
@Inject
public MyExecutionContext(ActorSystem actorSystem) {
// uses a custom thread pool defined in application.conf
super(actorSystem, "my.dispatcher");
}
}
然后需要在application.conf中定义一个custom dispatcher,详见Akka的文档
akka {
my.dispatcher {
type = Dispatcher
executor = "thread-pool-executor"
thread-pool-executor {
fixed-pool-size = 32
}
throughput = 1
}
}
(不是很了解application.conf的读取方式,经测试my.dispatcher不妨在akka放在根目录下也能正常执行)在拥有自己的dispatcher之后,可以显示的添加executor然后使用HttpException.fromThread进行封装
public class Application extends Controller {
private MyExecutionContext myExecutionContext;
@Inject
public Application(MyExecutionContext myExecutionContext) {
this.myExecutionContext = myExecutionContext;
}
public CompletionStage<Result> index() {
// Wrap an existing thread pool, using the context from the current thread
Executor myEc = HttpExecution.fromThread((Executor) myExecutionContext);
return supplyAsync(() -> intensiveComputation(), myEc)
.thenApplyAsync(i -> ok("Got result: " + i), myEc);
}
public int intensiveComputation() { return 2;}
}
即使使用了CompletionStage也不可能将同步的IO转换为异步的IO。如果不能避免使用某些阻塞方法(这些方法在某些时间点总会执行,然后某些线程就会被阻塞),除了将操作封装在CompletionStage,还需要添加一个拥有足够线程的隔离的执行上下文来处理并发。可以在[这里](https://playframework.com/documentation/2.6.x/ThreadPools)查看更多与Play线程池有关的内容,也可以在[这里](https://playframework.com/download#examples)下载一个与数据库相关的例子 |
action默认都是异步的
Play中的action都是异步的,比如下面的例子,返回的Result在内部包含了一个承诺
public Result index() {
return ok("Got request " + request() + "!");
}
无论代码中返回的是Result还是CompletionStage,这两种返回方式在Play的内部处理都是一样的,Action只有异步这一种类型。返回CompletionStage是一种写非阻塞代码的技术。 |
处理超时
在非阻塞的超时中,可以用play.libs.concurrent.Futures.timeout
来封装一个CompletionStage
class MyClass {
private final Futures futures;
private final Executor customExecutor = ForkJoinPool.commonPool();
@Inject
public MyClass(Futures futures) {
this.futures = futures;
}
CompletionStage<Double> callWithOneSecondTimeout() {
return futures.timeout(computePIAsynchronously(), Duration.ofSeconds(1));
}
public CompletionStage<String> delayedResult() {
long start = System.currentTimeMillis();
return futures.delayed(() -> CompletableFuture.supplyAsync(() -> {
long end = System.currentTimeMillis();
long seconds = end - start;
return "rendered after " + seconds + " seconds";
}, customExecutor), Duration.of(3, SECONDS));
}
}
超时与取消并不相同, – 超时的情况下计算任然会继续,尽管结果不会返回 |