dask futures
这篇博客文章将向您展示从基于Java 7 and jucFuture的实现逐步过渡到以Scala解决方案编写的akka actor的样子。 这将需要四个步骤,分别是:
完整的代码存储库可以在github上找到。
应用程序
该应用程序是简单的ItemService ,它允许您获取由整数id标识的客户端拥有的所有Items 。 目标是建立有关不同项目的一些统计信息:
- 多少客户的商品价格为x
- 多少件商品的价格为价格x
ItemService (理论上)连接到数据库,该数据库需要更长的时间来加载数据,因此我们不是顺序获取数据,而是并发获取数据。 最后,我们收集结果并计算统计数据。
去看了一下是怎么回事的ItemService打印出当前threadname他结束getItems(的clientId)调用之后。
Java 7和期货
第一个实现使用jucFuture和谷歌番石榴的ListenableFutures提供的一些功能性糖。 首先要做的是实现一个Callable <List <Item >> ,它可以通过jucExecutorService启动。
public static class ItemLoader implements Callable<List> {
private final int clientId;
public ItemLoader(int clientId) {
this.clientId = clientId;
}
@Override
public List call() throws Exception {
ItemService service = new ItemService();
return service.getItems(clientId);
}
没有什么花哨。 一个ItemLoader就像一个工作,它通过其构造函数进行配置(我应该为哪个客户端加载项目),然后实例化ItemService并获取项目。
现在创建一个ExecutorService并提交作业( ItemLoader实例)。
List<Integer> clients = ...;
// I tried some different ExecutorServices just for fun (and later some benchmarks, hopefully)
int parallelism = 4;
ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newWorkStealingPool(parallelism));
// Submit all the futures
List<ListenableFuture<List>> itemFutures = new ArrayList<>();
for (Integer client : clients) {
ListenableFuture<List> future = pool.submit(new ItemLoader(client));
itemFutures.add(future);
}
MoreExecutors.listeningDecorator(..)调用来自番石榴,番石榴装饰了我们的初始ExecutorService 。 这使我们可以使用从“类型项的期货列表”到“ 类型项的列表的未来”的非常整洁的过渡。
// Futures == com.google.common.util.concurrent.Futures
// convert list of futures to future of results
ListenableFuture<List<List<Item>>> resultFuture = Futures.allAsList(itemFutures);
// blocking until finished - we only wait for a single Future to complete
List<List<Item>> itemResults = resultFuture.get();
您可能会注意到我们有一个列表清单。 这是因为ItemService返回项目列表。 幸运的Google番石榴使用Iterables.concat再次帮助了我们, Iterables.concat通常被称为扁平化函数语言。 此操作将类型A的列表列表平化为类型A的列表。
Iterable items = Iterables.concat(itemResults);
从这里开始,您可以使用项目列表执行任何操作。
专业版 | 对比 |
---|---|
易于实施 | 没有失败处理(什么作业失败了?) |
轻松配置并行性(ExecutorService) | 阻塞 |
冗长的 |
Java 8流
接下来,我们将使用新的超棒Java 8功能并行流 。 这种用法感觉很像scala并行集合 ,这就是为什么我现在仅看一下Java 8功能的原因,因为我们有更多的scala并发/并行编程工具。
谈话很便宜,给我看看代码:
List clients = ...;
// create a parallel stream from the list of clients and map each of them to a ItemService call
Stream<List<Item>> serviceResults = clients.parallelStream()
.map(client -> new ItemService().getItems(client));
// flatten a stream of lists of type Item to a steam of type Item
Stream items = serviceResults.flatMap(itemList -> itemList.stream());
恕我直言,扁平化流的语法在我看来有点奇怪,但这是要走的路 。 但这是代码!
专业版 | 对比 |
---|---|
简短而富有表现力的实施 | 没有失败处理(什么作业失败了?) |
阻塞 | |
难以配置并行性 |
斯卡拉和期货
现在我们进入Scala宇宙。 正如我上面提到的,我将跳过scala并行集合,因为它们与java 8中的并行流非常相似。实际上,基于将来的实现也非常相似,但是我认为这是进入scala并发世界的一个更好的起点。
首先,我们需要一个ExecutionContext ,它类似于ExecutorService 。 实际上,您可以从ExecutorServices创建ExecutionContexts 。 对于此小型应用程序,我们使用标准的fork-join-pool。
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{ Await, Future }
import scala.concurrent.duration._
现在,我们可以直接在代码内部创建Futures。
val clients = 1 until 10 toSeq // start the futures val itemFutures: Seq[Future[Seq[Item]]] = clients map { client => Future { new ItemService getItems client } }
我写出了显式类型,以便您可以看到此处发生的情况。 Future声明主体中的所有内容都将在一个新的匿名Future中执行。 现在,我们再次将期货列表转换为列表的期货。 Scala开箱即用。
// convert list of futures to future of results val resultFuture: Future[Seq[Seq[Item]]] = Future sequence itemFutures
下一步是应该从此实现中删除的最重要的一步,因为到目前为止,java 7实现并没有真正的区别。 在scala中,您可以调用Future上的map ,它返回一个新的Future,并根据您的map函数映射结果值。 这有助于您编写非阻塞且可读的代码,因为:
- 您不必将完整的逻辑写入一个未来,而可以将其分解为不同的方法。 您还可以基于单个加载期货来更改不同的结果期货
- 您不必等待结果转换它们
关于一行代码的讨论很多。 我们将列表列表弄平。
// flatten the result val itemsFuture: Future[Seq[Item]] = resultFuture map (_.flatten)
毕竟,在此示例中,我们必须等待结果可用。
// blocking until all futures are finished, but wait at most 10 seconds val items = Await.result(itemsFuture, 10 seconds)
如果您不需要等待并且可以在某个时间点处理结果,请查看scala futures文档中描述的回调函数。 这些提供了对失败做出React的可能性。 对于期货和超时,您可以在此处阅读另一篇博客文章 。
专业版 | 对比 |
---|---|
简短而富有表现力的实施 | 仅在回调中处理失败 |
可以设为非阻塞 | 如果阻止,则必须定义很多超时 |
Scala和具有Ask模式的演员
如果不关心错误处理和容错能力,则以上所有解决方案均有效。 在某些情况下可以这样做,但也不需要太多努力! 而这是演员进来,如果你不知道什么演员都是通过滚动这些幻灯片从乔纳斯·鲍纳的阿卡的创造者,这应该给你足够的内部。
第一个实现将使用Ask模式 ,该模式根据您发送的消息生成期货 。 这通常需要在参与者系统的边界到代码的非基于参与者的部分。 总的来说,我获得了将边界移向“在演员系统内部做所有可能的事情”的经验。 通过这种方式,您将获得最大的收益。
让我们看一下我们的actor实现:
package actors
import akka.actor.Actor
import services.scala.{ Item, ItemService }
import ItemServiceActor._
class ItemServiceActor extends ItemService with Actor {
def receive = {
case GetItems(client) => sender ! getItems(client) // async answer
}
}
/** Message API */
object ItemServiceActor {
case class GetItems(client: Int)
}
基本上,它将我们现有的服务包装为演员。 您可能会问自己,为什么我在调用新服务时总是实例化它。 好吧,服务本身无论如何都不是线程安全的。 但是,现在由于服务是由actor表示的,因此它是自动线程安全的,因为actor保证一次只能处理一条消息(在这种情况下,这意味着对getItems()的方法调用)。
现在,我们进行管道测试,并要求ItemServiceActor提供项目列表。
// Create the actor system which manages all the actors
val system = ActorSystem()
val itemService = system.actorOf(Props[ItemServiceActor], "itemService")
// available clients
val clients = 1 until 10 toSeq
// how long until the ask times out
implicit val timeout = Timeout(10 seconds)
// start the futures
val itemFutures: Seq[Future[Seq[Item]]] = clients map { client =>
// this is the ask: itemService ? GetItems(client)
(itemService ? GetItems(client)).mapTo[Seq[Item]]
}
该代码几乎是不言自明的,需要mapTo [Seq [Item]] 。 目前,akka raw actor的实现不提供任何类型的消息。 因此,ask始终会返回类型为Any的future,然后通过mapTo调用将其映射到您想要的特定类型。 其余代码与scala和futures代码相似。
但是错误处理呢? 询价模式提供了比正常期货更多的功能 。 在后续博客文章中,我将向您展示如何处理易碎的ItemService 。 简而言之,您可以这样做:
val future = akka.pattern.ask(actor, msg1) recover {
case e: ArithmeticException => 0
}
专业版 | 对比 |
---|---|
线程安全服务(需要更少的实例) | 询问超时 |
Scala和演员(几乎)没有询问模式
正如我在上一章中提到的那样,将边界移到执行actor系统内部的所有事情是一件好事,我们将做到这一点。 通过这样做,我们还将了解akka的不同而强大的错误处理策略(或actor模型本身)。
此实现在消息API方面需要更多一点,因此我们从这里开始。
/**
* Defining the aggregation API
*/
object ItemServiceAggregator {
// ---- PUBLIC ----
case class GetItemStatistics(clients: Seq[Int])
case class ItemStatistics(results: Seq[(Int, Seq[Item])])
// ---- INTERNAL ----
private[actors] case class GetItems(client: Int, job: Long)
private[actors] case class Items(client: Int, items: Seq[Item], job: Long)
private[actors] case class Job(id: Long, source: ActorRef, clients: Seq[Int], results: Seq[(Int, Seq[Item])] = Seq.empty) {
def isFinished(): Boolean = results.size == clients.size
}
/** Worker actor similar to ask pattern ServiceActor */
class ItemServiceActor extends ItemService with Actor {
def receive = {
case GetItems(client, job) => sender ! Items(client, getItems(client), job) // async answer
}
}
}
主要区别在于:
- 结果案例类ItemStatistics ,其中包含具有(客户,项目)的序列
- 案例类Job封装了GetItemStatistics请求的状态
- 实际的ItemServiceActor几乎相同,但是保留了作业ID
现在到实际的ItemServiceAggregator ,它开始工作并将工作分配给ItemServiceActor 。
package actors
import akka.actor._
import akka.routing.RoundRobinPool
import scala.collection.mutable.{ Map => MutableMap }
import services.scala.ItemService
import services.scala.Item
import ItemServiceAggregator._
class ItemServiceAggregator extends Actor with ActorLogging {
// Create a pool of actors with RoundRobin routing algorithm
val worker = context.system.actorOf(
props = Props[ItemServiceAggregator.ItemServiceActor].withRouter(RoundRobinPool(10)),
name = "itemService"
)
/** aggregation map: (request, sender) -> (client, items) */
val jobs = MutableMap[Long, Job]()
// A VERY basic jobId algorithm
var jobId = 0L
def receive = {
case GetItemStatistics(clients) =>
jobId += 1
jobs(jobId) = Job(jobId, sender(), clients)
log info s"Statistics for job [$jobId]"
// start querying
clients foreach (worker ! GetItems(_, jobId))
// Get results from a job
case Items(client, items, jobId) =>
val lastJobState = jobs(jobId)
val newJobState = lastJobState.copy(
results = lastJobState.results :+ (client, items)
)
if (newJobState isFinished ()) {
// send results and remove job
newJobState.source ! ItemStatistics(newJobState.results)
jobs remove jobId
} else {
// update job state
jobs(jobId) = newJobState
}
}
}
聚合器的逻辑基于以下几个步骤:
- 收到一条GetItemStatistics(clients)消息。
- 通过增加jobId计数器来启动新作业,并将作业状态存储在可变映射中
- 接收项目结果作为消息。
- 如果已请求所有客户,则发送结果,否则仅汇总结果
基于此方案,您可以根据需要轻松添加更多错误处理。 对于每个作业错误处理,可以为每个作业创建一个JobActor ,它调用setReceiveTimeout ,当空闲时间太长时,它将向actor发送ReceiveTimeout消息。
摘要
首先,使用适合您需求的实现! 如果您不需要全面的错误处理或细粒度的调度逻辑,请先尝试简单的方法。 Scala期货真的很简单而且功能强大。 如果您想使用更多语法糖,请查看SIP-22 – Async ,它可以帮助您在使用期货时减少编写代码。
如果您的应用程序增长超出了最初的预期,那么从容易开始也可以使用。 使用Scala Futures可以轻松切换到actor,因为您可以轻松地将逻辑提取到actor中并使用Ask模式获得相同的Future。 然后,您可以根据需要逐步在内部重构actor。
翻译自: https://www.javacodegeeks.com/2014/11/from-java-7-futures-to-akka-actors-with-scala.html
dask futures