异步results
事实上整个Play框架都是异步的。Play非阻塞地处理每个request请求。
默认的配置适配的正是异步的controller。因此开发者应该尽力避免在在controller中阻塞,如在controller方法中等待其他的操作。常见的例子如:JDBC调用、流式API、HTTP请求以及长时间的计算等。
虽然可以通过提供并发线程数来允许阻塞式的controller来处理更多的请求,但是使用异步controller在高负载时的扩展性及简便性上更有优势。
创建非阻塞的actions
Play的工作模式决定了Action的速度必须要尽可能的快(非阻塞)。那么当我们还没有生成返回值时如何返回呢?其实 response 是一个 future result!
Future[Result] 代表未来某时刻的Result。通过返回 Future 来取消当前的阻塞,Play将会尽可能快的返回真正的Result。
虽然web客户端在等待响应的时候会阻塞,但是服务端不会有任何阻塞,资源可以得到最大程度的重用。
仅仅使用 Future 不是全部!如果调用阻塞的API,例如JDBC,那么你需要在不同的 excutor 中运行自己的 ExecutionStage,而不是使用Play的 rendering 线程池。这时你可以创造一个带 custom dispatcher 引用的 play.api.libs.concurrent.CustomExecutionContext 子类:
import play.api.libs.concurrent.CustomExecutionContext
trait MyExecutionContext extends ExecutionContext
class MyExecutionContextImpl @Inject()(system: ActorSystem)
extends CustomExecutionContext(system, "my.executor") with MyExecutionContext
class HomeController @Inject()(myExecutionContext: MyExecutionContext, val controllerComponents: ControllerComponents) extends BaseController {
def index = Action.async {
Future {
// Call some blocking API
Ok("result of blocking call")
}(myExecutionContext)
}
}
想要优化自定义的线程池可以查看 ThreadPools。
如何创建 Future[Result]
创建 Future[Result] 之前我们必须首先创建另一个Future,用它来提供计算最终Result所需要的参数:
val futurePIValue: Future[Double] = computePIAsynchronously()
val futureResult: Future[Result] = futruePIValue.map { pi =>
Ok("PI value computed: " + pi)
}
Play 所有的异步API调用都会返回 Future,无论你用 play.api.libs.WS 调用外部接口,或者利用Akka来调度异步任务,还是使用 play.api.libs.Akka 来和 actores通信。
下面是异步获取Future的一个代码块例子:
val futureInt: Future[Int] = scala.concurrent.Future {
intensiveComputation()
}
注意:理解线程返回future的部分是很重要的。上面两段代码中,默认导入了Play的execution context。它实际上是以回调作为参数的 future API 接收到的一个隐式参数。此execution context经常可以等同于一个线程池,虽然这并不是必须的。
你可以使用Future来将同步IO简单地包装为异步。但如果你无法从application架构上调整来避免阻塞,在某一时刻它仍然会被阻塞住。想将操作彻底异步化,你需要使用一个独立的execution context,并为之配置好足够的线程以应对并发需求。请查看 理解Play线程池 了解细节, play模板样例 展示了数据库集成。
Actor对于阻塞操作仍然是有意义的。它提供了整洁的方式来处理超时和异常,设置阻塞的execution context,以及管理服务相关的状态。此外,Actor还提供了ScatterGatherFirstCompletedRouter来处理同步缓存和数据库请求,并允许在后端服务器集群上远程执行代码。当然是否使用Actor要取决于你的需求,也不要过度的使用它。
返回Future
目前为止我们只用 Action.apply 构造器方法来构建action,为了发送异步result,我们需要使用 Action.async 构造器方法:
def index = Action.async {
val futureInt = scala.concurrent.Future { intensiveComputation()}
futureInt.map(i => Ok("Got result: " + i))
}
默认的Action是异步的
Play actions 是默认异步的。例如下面的controller, { Ok(...)} 并不是 controller 的方法体。它是传递到Action object的apply方法的一个匿名函数,将由它创建Action。Play内部会调用你创建的匿名函数并将返回的result封装进Future。
def echo = Action { request =>
Ok("Got request [" + request + "]")
}
注意:不管是Action.apply还是Action.async创建的Action其内部机制都是一样的。仅仅只有一种异步Action,而不是同步和异步两种。.async构造器仅仅是返回Future的简单方式,它让你更容易地写出非阻塞的代码。
处理超时
超时处理的重要性不言而喻,它可以避免错误发生时的网络阻塞。你可以使用 play.api.libs.concurrent.Futures 来包装Future,为其增加非阻塞的超时处理。
import scala.concurrent.duration._
import play.api.libs.concurrent.Futures._
def index = Action.async {
// You will need an implicit Futures for withTimeout() -- you usually get
// that by injecting it into your controller's constructor
intensiveComputation().withTimeout(1.seconds).map {
Ok("Got result: " + i)
}.recover {
case e: scala.concurrent.TimeoutException =>
InternalServerError("timeout")
}
}
注意:超时和取消是不同的 —— 超时仍然属于完成(complete),即使完成的值没有被返回。