欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

Akka最佳实践-在Actor伴生对象内提供Props的工厂方法

程序员文章站 2022-03-31 09:30:39
Akka最佳实践-在Actor伴生对象内提供Props的工厂方法 版权声明:本文为博主原创文章,未经博主允许不得转载。 手动码字不易,请大家尊重劳动成果,谢谢 作者...

Akka最佳实践-在Actor伴生对象内提供Props的工厂方法

版权声明:本文为博主原创文章,未经博主允许不得转载。
手动码字不易,请大家尊重劳动成果,谢谢
作者:https://blog.csdn.net/wang_wbq

在Akka官方文档中关于创建Actor部分提供了三种创建Actor的Props的方式:

1、val props1 = Props[MyActor]
2、val props2 = Props(new ActorWithArgs(“arg”)) // careful, see below
3、val props3 = Props(classOf[ActorWithArgs], “arg”) // no support for value class arguments

之后,官方文档推荐了一种创建actor的最佳方式,就是在Actor伴生对象内提供Props的工厂方法

并且文档中提到了:

It is a good idea to provide factory methods on the companion object of each Actor which help keeping the creation of suitable Props as close to the actor definition as possible. This also avoids the pitfalls associated with using the Props.apply(...) method which takes a by-name argument, since within a companion object the given code block will not retain a reference to its enclosing scope.

大概说的是推荐在actor对象的伴生对象里提供创建每个actor的Props的工厂方法,原因有两个:

1、离对应的actor更近,这个大家都能理解
2、避免了在使用Props.apply(...)只有一个传名参数的那个重载方法所产生的闭包问题

第一点比较好理解,离actor更近的话,封装性好、可维护性好等等一系列好处。

第二点乍看起来可能会让人产生迷惑,下面我就来详细解释下我对这句话的理解,如有问题还请指正。

我们把第二点摘出来:

This also avoids the pitfalls associated with using the Props.apply(...) method which takes a by-name argument, since within a companion object the given code block will not retain a reference to its enclosing scope.

首先,这句话指出了是使用了一个by-name argumentProps.apply(...),也就是最开始所说的第二种创建actor的方式,这个Props.apply代码原型如下:

def apply[T <: Actor: ClassTag](creator: ? T): Props =
    mkProps(implicitly[ClassTag[T]].runtimeClass, () ? creator)

Scala中的传名参数和传值参数

by-name argument(传名参数)是scala中的一个概念,也就是 :=>,一个微笑表情,越看越萌。与它相关的概念是by-value argument(传值参数)。

举个栗子,这是传值参数,和我们平时写的函数一毛一样:

def byValue(str: String): Unit ={
  println("before")
  println(str)
  println("after")
}

byValue{
  println("inner")
  "hello"
}
这段代码运行结果如下,是不是和你想的一样:
inner
before
hello
after
以下是传名参数,只有函数参数这一点不同:
def byName(str: => String): Unit ={
  println("before")
  println(str)
  println("after")
}

byName{
  println("inner")
  "hello"
}
运行结果发生了改变:
before
inner
hello
after

是不是很神奇,加了一个符号 inner就真的进到里面去了。

传值和传名的区别在于传名是在被引用处执行,而传值会在函数入口处调用。如果传名参数在函数内没有被用到,那它对应的代码块将不会被调用。

传名参数提供了懒加载的功能,它是Stream的基础,Stream是函数式编程的基础,因此这个特性非常重要。

Props.apply中的陷阱

我们了解了传名参数,我们就可以开始分析这个 def apply[T <: Actor: ClassTag](creator: ? T): Props方法了。它接受一个new MyActor(parameters)作为参数。我们可以看到它的参数creator被标记为传名参数,因此这个MyActor并不会立即被构造,而是在Akka内部真正需要创建这个actor的时候才会被调用。这样问题就来了,万一在Akka还没来得及调用这个构造函数的时候,paramters被改变了怎么办?这也就是官方文档中提到的第一点:

This also avoids the pitfalls associated with using the Props.apply(...) method which takes a by-name argument
避免了使用一个传名参数的Props.apply(...)方法时会遇到的陷阱。

我们针对这个陷阱来举个例子瞧瞧:

class ParentActor extends Actor {
  var index: Int = 0
  override def receive = {
    case "create" => {
      index += 1
      context.actorOf(Props(new ChildActor(index)))
    }
  }
}

final class ChildActor(value: Int) extends Actor {
  println(value + "\t" + self)
  override def receive = Actor.emptyBehavior
}
我们首先创建一个父actor,用以管理我们的子actor,为了方便管理,我们使用一个index变量来为子actor进行计数,每创建一个子actor就加1。 因为在Akka入门指南部分文档中我们已经提到了每个actor在同一时刻只会运行一个消息,因此我们不需要考虑并发的情况,也不需要为index做任何保护,一切都是这么顺理成章,对吧。 接下来我们写个main函数运行下试试:
def main(args: Array[String]): Unit = {
  val actorSystem = ActorSystem.create("test")
  val parentActor = actorSystem.actorOf(Props[ParentActor])
  parentActor ! "create"
  parentActor ! "create"
  parentActor ! "create"
  parentActor ! "create"
  parentActor ! "create"
}
还是很简单对不,我们先创建父actor,然后让父actor去创建5个子actor。我们预期的输出结果是啥,12345嘛,还能怎样?然而我得到了:
4   Actor[akka://test/user/$a/$c#55550344]
2   Actor[akka://test/user/$a/$a#-419491615]
2   Actor[akka://test/user/$a/$b#-584634733]
5   Actor[akka://test/user/$a/$d#-981644178]
5   Actor[akka://test/user/$a/$e#934288974]
这是啥?一定是我晕了,再跑一次:
2   Actor[akka://test/user/$a/$a#1579045628]
5   Actor[akka://test/user/$a/$b#-1972515577]
5   Actor[akka://test/user/$a/$c#1187560733]
5   Actor[akka://test/user/$a/$e#1848548968]
5   Actor[akka://test/user/$a/$d#1243827873]

这又是啥,居然每次跑还不一样?

有了我们之前对传名参数的了解,我们意识到可能actorOf并没有同步被调用,这里面的细节我还没有跟踪进去,待后续了解后再另开贴详细解释下吧。

回到这里例子,在context.actorOf(Props(new ChildActor(index)))这句代码中,我们把Props(new ChildActor(index))作为传名函数传人了actorOf方法,特别的是,在这个函数闭包中,引用到了外层actor的index成员 变量!变量!变量!重要的事情说三遍。由于我们不知道actorOf方法的实现细节,所以这个闭包函数的调用时刻我们也没法确定。但从运行结果来看,确实不是同步的。在我们第二次创建actor前,第一个actor还没有被创建,但是它闭包中的index已经被改变了,因此问题就出现了。所以为啥Scala这么鼓励大家使用val而不是var,这都是有原因的嘛。

为了解决这个问题,很简单嘛,经常做前台的程序猿都知道,加载错误了,就加个延时瞧瞧嘛:

def main(args: Array[String]): Unit = {
  val actorSystem = ActorSystem.create("test")
  val parentActor = actorSystem.actorOf(Props[ParentActor])
  parentActor ! "create"
  Thread.sleep(10)
  parentActor ! "create"
  Thread.sleep(10)
  parentActor ! "create"
  Thread.sleep(10)
  parentActor ! "create"
  Thread.sleep(10)
  parentActor ! "create"
}

不知道你们那怎么样,反正我这是好了:

1   Actor[akka://test/user/$a/$a#1304857964]
2   Actor[akka://test/user/$a/$b#-493158798]
3   Actor[akka://test/user/$a/$c#-322808583]
4   Actor[akka://test/user/$a/$d#-2080202987]
5   Actor[akka://test/user/$a/$e#267901210]

但是,为啥加10毫秒就好了,5毫秒可以吗?和前台一样,慢慢调喽。但是这只是暂时规避了这个问题,并没有从根本去解决。

推荐方式

为了从根本解决这个传名参数的问题,也很简单嘛,都说了规避传名参数,改成传值不就好了。从外面包个传值的函数不就解决问题了吗。

class ParentActor extends Actor {
  var index: Int = 0
  override def receive = {
    case "create" => {
      index += 1
      createChild(index)
    }
  }

  def createChild(idx: Int): Unit ={
    context.actorOf(Props(new ChildActor(idx)))
  }
}

好了,问题解决了:

1   Actor[akka://test/user/$a/$a#1625610590]
2   Actor[akka://test/user/$a/$b#-1625962984]
3   Actor[akka://test/user/$a/$c#-2045097200]
4   Actor[akka://test/user/$a/$d#480038635]
5   Actor[akka://test/user/$a/$e#616683299]
结合我们之前第一条推荐:最好把actor的Props创建放在其伴生对象中,有什么什么的一堆好处是吧。因此我们再改造一下,最终变成了这个样子:
class ParentActor extends Actor {
  var index: Int = 0
  override def receive = {
    case "create" => {
      index += 1
      createChild(index)
    }
  }

  def createChild(idx: Int): Unit ={
    context.actorOf(ChildActor.props(idx))
  }
}

object ChildActor {
  def props(index: Int) = Props(new ChildActor(index))
}

final class ChildActor(value: Int) extends Actor {
  println(value + "\t" + self)
  override def receive = Actor.emptyBehavior
}

这正是Akka官方文档中推荐的actor创建方式: 在Actor伴生对象内提供Props的工厂方法

在看官方文档中的后半段:

since within a companion object the given code block will not retain a reference to its enclosing scope.
伴生对象不会在它的作用域里保持着外部的引用(引用类型对象除外对吧)
好了,到此为止,对Akka文档中推荐的actor创建方式已经按我的理解解释完毕。