Akka(6): become/unbecome:运算行为切换

   通过一段时间的学习了解,加深了一些对Akka的认识,特别是对于Akka在实际编程中的用途方面。我的想法,或者我希望利用Akka来达到的目的是这样的:作为传统方式编程的老兵,我们已经习惯了直线流程方式一口气实现完整的功能。如果使用Akka,我们可以把这个完整的功能分切成多个能产生中间临时结果的小功能然后把这些功能放到不同的Actor上分别独立运算,再通过消息来连接这些功能集合成最终结果。如此我们就轻易得到了一个多线程并发程序。由于Akka是软件工具(Tool),没有软件架构(Framework)对编程方式的特别要求,Actor的构建和使用非常方便,我们甚至不需要多少修改就可以直接把原来的一段代码移到Actor上。如果遇到一些重复的运算,我们还可以用Routing来实现并行运算。当然,把Actor当作简单的行令运算器可能还不够,如果能实现一些具体运算之上的高层次程序逻辑和流程就更加完善。我们可以用这样的高层次Actor去解析程序逻辑、执行流程、把具体的运算分配给其它各种运算Actor或者一组Routees并行运算从而取得整体程序的高效率运行。具备了这些功能后,也许我们就可以完全用Actor模式来替代传统单线程行令编程了。Akka可以通过Actor的动态行为转换来实现同一Actor在不同情况下提供不同的功能支持。我们前面提到Actor的功能是在receive函数内实现的。那么转换功能是否就是切换不同的receive函数呢?答案是确定的,Akka是通过Actor的context.become(rcvFunc)来实现receive函数切换的,我们看看下面这个示范:

import akka.actor._

object FillSeasons {
  case object HowYouFeel
  def props = Props(new FillSeasons)
}

class FillSeasons extends Actor with ActorLogging {
  import FillSeasons._

  override def receive: Receive = spring
  def Winter: Receive = {
    case HowYouFeel =>
      log.info("It's freezing cold!")
  }
  def summer: Receive = {
    case HowYouFeel =>
      log.info("It's hot hot hot!")
  }
  def spring: Receive = {
    case HowYouFeel =>
      log.info("It feels so goooood!")
  }
}

object Becoming extends App {
  val demoSystem = ActorSystem("demoSystem")

  val feelingsActor = demoSystem.actorOf(FillSeasons.props,"feelings")

  feelingsActor ! FillSeasons.HowYouFeel

}

在FeelingsActor里我们定义了三个receive函数,对共同的HowYouFeel消息采取了不同的反应。默认行为是spring。那么应该如何在三种行为中切换呢?用context.become(???),如下:

import akka.actor._

object FillSeasons {
  case object HowYouFeel
  case object ToSummer
  case object ToSpring
  case object ToWinter
  def props = Props(new FillSeasons)
}

class FillSeasons extends Actor with ActorLogging {
  import FillSeasons._

  override def receive: Receive = spring
  def winter: Receive = {
    case HowYouFeel =>
      log.info("It's freezing cold!")
    case ToSummer => context.become(summer)
    case ToSpring => context.become(spring)

  }
  def summer: Receive = {
    case HowYouFeel =>
      log.info("It's hot hot hot!")
    case ToSpring => context.become(spring)
    case ToWinter => context.become(winter)
  }
  def spring: Receive = {
    case HowYouFeel =>
      log.info("It feels so goooood!")
    case ToSummer => context.become(summer)
    case ToWinter => context.become(winter)
  }
}

object Becoming extends App {
  val demoSystem = ActorSystem("demoSystem")

  val feelingsActor = demoSystem.actorOf(FillSeasons.props,"feelings")

  feelingsActor ! FillSeasons.HowYouFeel
  feelingsActor ! FillSeasons.ToSummer
  feelingsActor ! FillSeasons.HowYouFeel
  feelingsActor ! FillSeasons.ToWinter
  feelingsActor ! FillSeasons.HowYouFeel
  feelingsActor ! FillSeasons.ToSpring
  feelingsActor ! FillSeasons.HowYouFeel

  scala.io.StdIn.readLine()
  demoSystem.terminate()
}

我们增加了三个消息来切换receive。运算结果如下:

[INFO] [06/08/2017 17:51:46.013] [demoSystem-akka.actor.default-dispatcher-3] [akka://demoSystem/user/feelings] It feels so goooood!
[INFO] [06/08/2017 17:51:46.019] [demoSystem-akka.actor.default-dispatcher-4] [akka://demoSystem/user/feelings] It's hot hot hot!
[INFO] [06/08/2017 17:51:46.028] [demoSystem-akka.actor.default-dispatcher-4] [akka://demoSystem/user/feelings] It's freezing cold!
[INFO] [06/08/2017 17:51:46.028] [demoSystem-akka.actor.default-dispatcher-4] [akka://demoSystem/user/feelings] It feels so goooood!


Process finished with exit code 0

就这样在几个receive里窜来窜去的好像已经能达到我们设想的目的了。看看Akka源代码中become和unbecome发现这样的做法是不正确的:

  def become(behavior: Actor.Receive, discardOld: Boolean = true): Unit =
    behaviorStack = behavior :: (if (discardOld && behaviorStack.nonEmpty) behaviorStack.tail else behaviorStack)

  def become(behavior: Procedure[Any]): Unit = become(behavior, discardOld = true)

  def become(behavior: Procedure[Any], discardOld: Boolean): Unit =
    become({ case msg ⇒ behavior.apply(msg) }: Actor.Receive, discardOld)

  def unbecome(): Unit = {
    val original = behaviorStack
    behaviorStack =
      if (original.isEmpty || original.tail.isEmpty) actor.receive :: emptyBehaviorStack
      else original.tail
  }

从上面的代码可以发现:调用become(x)实际上是把x压进了一个堆栈里。如果像我们这样不断调用become转来转去的,在堆栈上留下旧的行为函数实例最终会造成StackOverFlowError。所以Akka提供了unbecome,这是个堆栈弹出函数,把上一个become压进的行为函数再弹出来,释放一个堆栈空间。所以我们应该用unbecome来解决堆栈溢出问题。但是,如果在多个receive函数之间转换来实现行为变化的话,就难以正确掌握堆栈的压进,弹出冲抵配对,并且无法避免所谓的意大利面代码造成的混乱逻辑。所以,become/unbecome最好使用在两个功能之间的转换。我们再设计一个例子来示范:

sealed trait DBOperations
case class DBWrite(sql: String) extends DBOperations
case class DBRead(sql: String) extends DBOperations

sealed trait DBStates
case object Connected extends DBStates
case object Disconnected extends DBStates

DBoperations代表数据库读写操作。DBState代表数据库当前状态:连线Connected或断线Disconnected。只有数据库在Connected状态下才能进行数据库操作。顺理成章,我们需要两个receive函数:

import akka.actor._
sealed trait DBOperations
case class DBWrite(sql: String) extends DBOperations
case class DBRead(sql: String) extends DBOperations

sealed trait DBStates
case object Connected extends DBStates
case object Disconnected extends DBStates

object DBOActor {
  def props = Props(new DBOActor)
}

class DBOActor extends Actor with ActorLogging {

  override def receive: Receive = disconnected

  def disconnected: Receive = {
    case Connected =>
      log.info("Logon to DB.")
      context.become(connected)
  }
  def connected: Receive = {
    case Disconnected =>
      log.info("Logoff from DB.")
      context.unbecome()
    case DBWrite(sql) =>
      log.info(s"Writing to DB: $sql")
    case DBRead(sql) =>
      log.info(s"Reading from DB: $sql")
  }
}

object BecomeDB extends App {
  val dbSystem = ActorSystem("dbSystem")
  val dbActor = dbSystem.actorOf(DBOActor.props,"dbActor")

  dbActor ! Connected
  dbActor ! DBWrite("Update table x")
  dbActor ! DBRead("Select from table x")
  dbActor ! Disconnected

  scala.io.StdIn.readLine()
  dbSystem.terminate()

}

运算结果显示如下:

[INFO] [06/09/2017 11:44:40.093] [dbSystem-akka.actor.default-dispatcher-3] [akka://dbSystem/user/dbActor] Logon to DB.
[INFO] [06/09/2017 11:44:40.106] [dbSystem-akka.actor.default-dispatcher-3] [akka://dbSystem/user/dbActor] Writing to DB: Update table x
[INFO] [06/09/2017 11:44:40.107] [dbSystem-akka.actor.default-dispatcher-3] [akka://dbSystem/user/dbActor] Reading from DB: Select from table x
[INFO] [06/09/2017 11:44:40.107] [dbSystem-akka.actor.default-dispatcher-3] [akka://dbSystem/user/dbActor] Logoff from DB.

以上是按正确顺序向dbActor发出数据库操作指令后产生的结果。但是,我们是在一个多线程消息驱动的环境里。发送给dbActor的消息收到时间无法预料。我们试着调换一下指令到达顺序:

  dbActor ! DBWrite("Update table x")
  dbActor ! Connected
  dbActor ! DBRead("Select from table x")
  dbActor ! Disconnected

运算结果:

[INFO] [06/09/2017 11:54:57.264] [dbSystem-akka.actor.default-dispatcher-4] [akka://dbSystem/user/dbActor] Logon to DB.
[INFO] [06/09/2017 11:54:57.273] [dbSystem-akka.actor.default-dispatcher-4] [akka://dbSystem/user/dbActor] Reading from DB: Select from table x
[INFO] [06/09/2017 11:54:57.273] [dbSystem-akka.actor.default-dispatcher-4] [akka://dbSystem/user/dbActor] Logoff from DB.

漏掉了DBWrite操作。可以理解,所有connected状态之前的任何操作都不会真正生效。Akka提供了个Stash trait能把一个receive函数未处理的消息都存起来。然后用unstash()可以把存储的消息都转移到本Actor的邮箱里。我们可以用Stash来解决这个消息遗失问题:

  def disconnected: Receive = {
    case Connected =>
      log.info("Logon to DB.")
      context.become(connected)
      unstashAll()
    case _ => stash()
  }

所有消息遗失都是在Disconnected状态内发生的。在disconnected里我们用stash把所有非Connected消息存起来,然后在转换成Connected状态时把这些消息转到信箱。再看看运算结果:

object BecomeDB extends App {
  val dbSystem = ActorSystem("dbSystem")
  val dbActor = dbSystem.actorOf(DBOActor.props,"dbActor")


  dbActor ! DBWrite("Update table x")
  dbActor ! Connected
  dbActor ! DBRead("Select from table x")
  dbActor ! Disconnected

  scala.io.StdIn.readLine()
  dbSystem.terminate()

}

[INFO] [06/09/2017 12:01:54.518] [dbSystem-akka.actor.default-dispatcher-4] [akka://dbSystem/user/dbActor] Logon to DB.
[INFO] [06/09/2017 12:01:54.528] [dbSystem-akka.actor.default-dispatcher-4] [akka://dbSystem/user/dbActor] Writing to DB: Update table x
[INFO] [06/09/2017 12:01:54.528] [dbSystem-akka.actor.default-dispatcher-4] [akka://dbSystem/user/dbActor] Reading from DB: Select from table x
[INFO] [06/09/2017 12:01:54.528] [dbSystem-akka.actor.default-dispatcher-4] [akka://dbSystem/user/dbActor] Logoff from DB.

显示结果正确。下面就是整个示范的源代码:

import akka.actor._
sealed trait DBOperations
case class DBWrite(sql: String) extends DBOperations
case class DBRead(sql: String) extends DBOperations

sealed trait DBStates
case object Connected extends DBStates
case object Disconnected extends DBStates

object DBOActor {
  def props = Props(new DBOActor)
}

class DBOActor extends Actor with ActorLogging with Stash {

  override def receive: Receive = disconnected

  def disconnected: Receive = {
    case Connected =>
      log.info("Logon to DB.")
      context.become(connected)
      unstashAll()
    case _ => stash()
  }
  def connected: Receive = {
    case Disconnected =>
      log.info("Logoff from DB.")
      context.unbecome()
    case DBWrite(sql) =>
      log.info(s"Writing to DB: $sql")
    case DBRead(sql) =>
      log.info(s"Reading from DB: $sql")
  }
}

object BecomeDB extends App {
  val dbSystem = ActorSystem("dbSystem")
  val dbActor = dbSystem.actorOf(DBOActor.props,"dbActor")


  dbActor ! DBWrite("Update table x")
  dbActor ! Connected
  dbActor ! DBRead("Select from table x")
  dbActor ! Disconnected

  scala.io.StdIn.readLine()
  dbSystem.terminate()

}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏阿凯的Excel

文件读取功能(Pandas读书笔记7)

前言:废话 之前宝宝出生,然后又忙着考试。 虽然考试很简单,但是必须要一次过,所以沉浸在两本书的海洋之中,好在天道酬勤,分别以自己满意的分数(87、81)通过了...

4045
来自专栏开源优测

基于Excel参数化你的Selenium2测试

前言 今天我们就如何使用xlrd模块来进行python selenium2 + excel自动化测试过程中的参数化进行演示说明,以解决大家在自动化测试实践过程中...

2746
来自专栏编程之旅

微信小程序——使用setData修改数组中的单个对象

微信小程序已经出来挺久的时间了,之前只是在文档上粗略的看了一下,最近稍得空闲,便利用微信小程序平台写一个练手的项目,顺便学习一下小程序开发,感觉大体跟前端开发基...

762
来自专栏张高兴的博客

张高兴的 UWP 开发笔记:横向 ListView

3227
来自专栏GreenLeaves

Js框架设计之DomReady

一、在介绍DomReady之前,先了解下相关的知识 1、HTML是一种标记语言,告诉我们这页面里面有什么内容,但是行为交互则要通过DOM操作来实现,但是注意:不...

2056
来自专栏hrscy

iOS 9 Storyboard 教程(二上)介绍Segue静态单元格(static cell)

Add Player 最终的设计看上去像下面这样:#接第一部分: 原帖地址 简书地址

661
来自专栏Hongten

使 Excel 规则更容易理解(Oracle Policy Modeling-Make your Excel rules easier to understand)

您可以使用图例关键字创建在 Excel 中引用属性的缩写方式。可以根据情况指定此缩写形式。

672
来自专栏恰同学骚年

自己动手写三层代码生成器学习总结

  今天看了下老杨的视频教程,写了一把三层代码生成器,理解了一下简单的代码生成器是如何实现的,其重点就在于数据库系统视图。

534
来自专栏君赏技术博客

Jekyll-Admin-Mac-模板处理

制作导航条 使用网络框架Alamofire 使用 SwiftyJSON iOS10开启启用 HTTP 请求 使用 ObjectMapper ...

483
来自专栏代码GG之家

google 分屏 横屏模式 按home键界面错乱故障分析(一)

你确定你了解分屏的整个流程? ? 之前分析文章列表: Android 关机对话框概率没有阴影故障分析 android recent key长按事件弹起触发最近列...

1818

扫码关注云+社区