dask futures_从Java 7 Futures到Scala的Akka演员

这篇博客介绍了如何从基于Java 7和jucFuture的实现过渡到Scala的Akka Actors解决方案。通过四个步骤展示了这个过程:Java 7和Futures、Java 8并行流、Scala和Futures以及Scala与Ask模式的Actors。文章通过一个ItemService应用演示并发获取数据并计算统计信息,最后提到了如何在Akka Actors中处理错误和容错。
摘要由CSDN通过智能技术生成

dask futures

这篇博客文章将向您展示从基于Java 7 and jucFuture的实现逐步过渡到以Scala解决方案编写的akka​​ actor的样子。 这将需要四个步骤,分别是:

  1. Java 7和期货
  2. Java 8和并行流
  3. Scala和期货
  4. Scala和具有Ask模式的演员
  5. Scala和演员(几乎)没有询问模式

完整的代码存储库可以在github上找到。

应用程序

该应用程序是简单的ItemService ,它允许您获取由整数id标识的客户端拥有的所有Items 。 目标是建立有关不同项目的一些统计信息:

  • 多少客户的商品价格为x
  • 多少件商品的价格为价格x

java7-scala-futures-流程图

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
      }
  }
 
}

聚合器的逻辑基于以下几个步骤:

  1. 收到一条GetItemStatistics(clients)消息。
  2. 通过增加jobId计数器来启动新作业,并将作业状态存储在可变映射中
  3. 接收项目结果作为消息。
  4. 如果已请求所有客户,则发送结果,否则仅汇总结果

基于此方案,您可以根据需要轻松添加更多错误处理。 对于每个作业错误处理,可以为每个作业创建一个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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值