异步是需要付出代价的,但是你最好知道你到底要付出什么。
让我们先讨论一些概念:
线程:代码在处理器上运行的基本单元。你可以为现有的或新的线程提供一些代码,然后执行它。通常你可以在一个JVM上运行上百个线程,并设计出成千上万的变量。通过多线程实现多任务的资源消耗非常的低。多线程可以存在于一个处理器上,多任务之间在这个处理器上通过线程切换,亦就是上下文切换,这就让我们觉得程序是在并行的运行一样。下面是在Java中直接,原始的使用Thread的例子。
public class MyRunnable implements Runnable { public void run(){ System.out.println("MyRunnable running"); } } Thread thread = new Thread(new MyRunnable()); thread.start();异步:非同步,或者说不能保证跟代码中的顺序一样去执行。这是很有可能发生,因为异步并不会保证顺序一样,所以你也不能这样认为。现在我们有这么一个定义,所以我们可以说Async也是如此的。异步代码没有并行处理的必要(并行处理可以最充分的利用多核处理器),也没有非阻塞的必要(通过回调事件,可以让线程得到充分利用),但是异步有可能并发处理,同时也带来吗发。基本上, 异步代码隔离是一个问题并且你应该知道为什么你要引入这么一个新问题。所以下次如果有人谈起这点, 你应该清楚认识到异步特性是要付出代价的,同时你最好了解你到底要付出什么代价,以确保你的成本是值得的,并且使用一些工具让你的成本可控。
非阻塞:当调用者(线程)调用的时候,不会导致调用者因等待结果而被阻塞,而是提供一个机制(通常和回调有关)每当结果准备好的时候,允许调用者执行一些动作。非阻塞常用于IO操作,因为这不涉及CPU, 或者线程,来参与完成整个任务。这种编程理念如果没有用到合适的架构,则可能会导致严重灾难。
Reactive编程: 以非阻塞方式编程。
Futures: 一种支持高度可组合的reactive 编程结构。基本上,当你决定使用异步和reactive风格编程(这有些优点,例如利用多核,非阻塞IO或者隔离执行)的时候,Futures提供一个机制,让得问题变得简单和更容易管理。它们的特点是语义组成和代表函数:让两个或以上的调用按顺序执行,错误捕捉,组合异步结果和提供大量的高级别的API,当使用大量的技术去执行必要的同步。并不是很直观地,Future并不一定在日后一定发生。当你接收到它的时候,它很可能已经发生。你甚至可以让已接收到值的Futures,使用Future.successful.
使用Futures的例子:
val f: Future[Long] = Future { fib(100) } val fib100:Future[Long] = Future.successful(354224848179261915075) val answerIsOk: Future[Boolean] = f.zip(fib100).map{ case (r1,r2) => r1 == r2 } val all: Future[List[Long]] = Future.sequence(List(f,fib100))
ExecutionContext: 被Futrues用于代表一个线程池。在谈到负责执行代码的线程池的时候,ExecutionContext 是一个很好的方法。ExecutionContext 可以共享线程或者处理器,这完全取决于ExecutionContext 如何实现。下面是使用ExecutionContext 的例子:
val f: Future[Long] = Future { fib(100) }( ExecutionContext.Implicits.global)让我们来分析Futrue的使用,你会发现每次你组合其他Future的时候,你会得到一个新的Future。那又如何?这似乎是我们不能抛弃Future并且得到它里面的信息。技术上,你可以使用Await去阻塞调用者,直到你得到一个结果。等待是否有坏处?很多时候,是。但是也有例外的情况,但你要清楚知道你在做什么。
好消息是:Play框架很好的支持Future,更精确的说应该是Future[Result]。这就意味着,你需要的就是把你的Future转换为Future Result,并且Play框架会在那里接管。这样是否说明了Play是异步的呢?Reactive?非阻塞的?支持并行处理?我们要经常使用Futures?可不可以不用Futures?可以阻塞么?SQL阻塞式调用怎么样?这些问题都是我想要在这解答的。首先,如下是在Play中处理Futrue:
def index = Action { Async { fib100.map( r => Ok("fib 100 is: " +r) ) } }
使用异步,允许我们在Play中处理Future,周期性的。如我们所知,Future并非是在以后一定会出现的。Play只是给我们一个机会去使用Future,而去不去使用这个机会全在于你。也就是说,可以在Play中选择非阻塞(reactive),但是Play不会强逼你去这样做。如果你偏向于同步(伴随很多的线程)或者你没办法判定异步代码的代价,这是非常好的选择。
请记住,虽然,API提供回调或者Future可能是非阻塞的(reactive)。例如,网页调用,网络,文件系统,一些数据库驱动,调度程序,事件等等。
Play定义了一个ExecutionContext负责去执行用户代码。通过它,Play可以很好的分开负责处理系统内部任务(处理文件,处理请求等等)的线程池和Play用户代码之间的线程池。也就是说,即使你的“用户代码ExecutionContext”已经没有可用线程了,系统仍能处理它自己的任务。这个永远成立,除非用户的代码占用了100%的CPU使用率。
“用户代码ExecutionContext”是可配置的,以下是配置线程池的一些建议:
用户代码ExecutionContext可以通过play.api.libs.concurrent.Execution.defaultContext去访问,但是通常我们不建议直接使用它。
除了分开Play的用户代码与内部任务,你还能分开应用中不同的部分,让它们在不同的线程池中运行。你可能会发现,让所有的操作使用同一个ExecutionContext就意味着,它们共享线程池,意味着,当一个动作阻塞线程可能会影响到其他正在执行纯CPU计算的动作!另外一个好处是,当我们把一个CPU密集计算的函数与其他应用隔离,那么你不怕因其中一个耗时的函数执行,而影响整个应用。想象一下,如果你的应用查找主页的时候非常的慢,因为另外一个不同的页面正在执行脸部识别,这该多么的糟糕。
为应用中不同的部分分配不同的线程池,有不同的方法。但是重点是ExecutionContexts,不是Futures,不是Async。
为不同的动作使用不同的ExecutionContext。一次性的方法是,使用Future,并把ExecutionContext当做另外的参数(隐式的)。我们可以这样定义:
// An ExecutionContext with a ThreadPool // that the same size as the ConnectionPool of our DB val dbExecutionContext: ExecutionContext = ... val eventuallySql = Future { /* some sql here */ }(dbExecutionContext) // hand the Future to Play def getUser = Action { Async { eventuallySql.map( user => Ok("user is: "+user)) } }很明显,现在使用Async并不重要,因为它并不能保证同步执行。而使用ExecutionContext可以避免阻塞应用中的其他操作。
既然我们经常使用ExecutionContext,为什么不写一个简单的API:
def DBAction(r:=> Result):EssentialAction = { Action { Async{ Future(r)(dbExecutionContext) } } }使用上面的API:
def getUSer = DBAction { val user = // get the user from the database Ok("user is: "+user) }现在上面例子的SQL 代码是阻塞的。但是这没问题,因为我们使用ExecutionContext把它和应用的其他部分分开了。
总的来说:
2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务