Akka Actor作为Scala并发编程中,最为关键的技术。对于一个学习Java编程的人来说,是一个非常优秀的并发编程框架,值得学习研究。
同时,在进行分布式学习的过程中,Akka也提供了相当友好的解决方案,所以掌握Actor技术是非常有价值的,它会帮助编程人员写出高性能、可伸缩、可测试的分布式应用。
得益于Actor告别了传统的Java并发编程,编程者不再需要使用底层的同步策略来去维护一个对象的状态(防止出现invalid状态)。告别锁、同步这些难以把握的Java特性,同时也不需要编程者编写高难度CAS代码(性能是不错),甚至一些non-blocked、asynchronous IO都已经被Akka IO解决好了。我们可以信赖依靠Akka,开发出更好的并发程序。
这边文章,我就Scala官网的quickstart案例,进行分析总结。如果英文水平允许,非常建议直接看官方文档,本文只是本人的个人观点。
进入正题。
(一)项目需求
在这个案例当中,我们要完成一个关于物联网的项目。我们现在有许多温度监控器,若干个温度监控器组成一个监控器组,系统将管理这些监控器组。在另一方面,用户可以实时获取制定温度监控器的信息,或者是监控器组的全部温度监控器的信息。下图是需求展示:
(二)项目框架
我们要使用Akka的Actor框架解决上面的问题。首先,我们先一起简单了解一下Actor的一些基础知识。
(1)Actor的层级结构
在上图中,/user对应的actor(通常名为system)是用户使用ActorSystem()方法创建出来的,system下面的所有actor都是用户自己创建的,完成用户特定的业务逻辑。从图中我们可以初步了解actorOf()方法是用来从一个actor中创建出一个新的actor。新创建的actor地址在父actor之下。
(2)Actor的生命周期
一个actor具有自己的声明周期,例如我们可以重写preStart(),使一个actor在被创建之前,调用该函数。类似的函数还有postStop()。
一个父actor停止运行,如下代码:context.stop(self),将会是其所有子actor结束执行。
(3)Actor的监控模式
(4)Actor的消息队列
(5)系统框架
(二)实现Device的Actor
(1)实现读协议
final case class ReadTemperature(requestId: Long)
final case class RespondTemperature(requestId: Long, value: Option[Double])
ReadTemperature作为请求消息,需要传入一个请求Id(并不知道有什么用)。Device会返回一个RespondTemperature作为数据消息,针对制定的请求Id,放回一个Option(温度可能不存在)。上述代码放在Device(actor)的伴生对象中,方便外部进行调用。
import akka.actor.{ Actor, ActorLogging, Props }
object Device {
def props(groupId: String, deviceId: String): Props = Props(new Device(groupId, deviceId))
final case class ReadTemperature(requestId: Long)
final case class RespondTemperature(requestId: Long, value: Option[Double])
}
class Device(groupId: String, deviceId: String) extends Actor with ActorLogging {
var lastTemperatureReading: Option[Double] = None
override def preStart(): Unit = log.info("Device actor {}-{} started", groupId, deviceId)
override def postStop(): Unit = log.info("Device actor {}-{} stopped", groupId, deviceId)
override def receive: Receive = {
case ReadTemperature(id) =>
sender() ! RespondTemperature(id, lastTemperatureReading)
}
}
在receive方法中,Device如果接收到一个ReadTemperature消息,它将会返回给发送者一个RespondTemperature消息。
(2)实现写协议
写协议的实现是类似的。import akka.actor.{ Actor, ActorLogging, Props }
object Device {
def props(groupId: String, deviceId: String): Props = Props(new Device(groupId, deviceId))
final case class RecordTemperature(requestId: Long, value: Double)
final case class TemperatureRecorded(requestId: Long)
final case class ReadTemperature(requestId: Long)
final case class RespondTemperature(requestId: Long, value: Option[Double])
}
class Device(groupId: String, deviceId: String) extends Actor with ActorLogging {
import Device._
var lastTemperatureReading: Option[Double] = None
override def preStart(): Unit = log.info("Device actor {}-{} started", groupId, deviceId)
override def postStop(): Unit = log.info("Device actor {}-{} stopped", groupId, deviceId)
override def receive: Receive = {
case RecordTemperature(id, value) =>
log.info("Recorded temperature reading {} with {}", value, id)
lastTemperatureReading = Some(value)
sender() ! TemperatureRecorded(id)
case ReadTemperature(id) =>
sender() ! RespondTemperature(id, lastTemperatureReading)
}
}
Device接受到RecordTemperature消息,更新自己的温度状态,并返回一个确认消息。
(3)测试部分
对于我们实现的Device进行测试。 测试Demo讲解"test1" should "reply with latest temperature reading" in {
val probe = TestProbe()
val deviceActor = system.actorOf(Device.props("group", "device"))
deviceActor.tell(Device.RecordTemperature(requestId = 1, 24.0), probe.ref)
probe.expectMsg(Device.TemperatureRecorded(requestId = 1))
deviceActor.tell(Device.ReadTemperature(requestId = 2), probe.ref)
val response1 = probe.expectMsgType[Device.RespondTemperature]
response1.requestId should ===(2)
response1.value should ===(Some(24.0))
deviceActor.tell(Device.RecordTemperature(requestId = 3, 55.0), probe.ref)
probe.expectMsg(Device.TemperatureRecorded(requestId = 3))
deviceActor.tell(Device.ReadTemperature(requestId = 4), probe.ref)
val response2 = probe.expectMsgType[Device.RespondTemperature]
response2.requestId should ===(4)
response2.value should ===(Some(55.0))
}
probe相当于一个actor,但是我们需要ActorRef,所以往往我们使用probe.ref。代码非常直观,不多做讲解了。