不要将Actors用于并发编程

15-01-04 banq
                   

将Scala/AKKA的Actor用于并发编程是一种反模式,相反,应该使用Actor模型守护状态,使用future实现并发,来自Don't use Actors for concurrency一文提出了自己独特观点:

在Scala领域,一个通用实践是使用Actor实现并发,这是受到Akka和许多Scala文章的影响,这些文章都是高度围绕Actor为中心(actor-centric),这是一种坏的实践,应该被认为是一种反模式,Actor不应该被作为流程控制或并发的工具,它们只是对以下两种目标适合:维护状态和提供消息端点,在其他场合最好可能使用Futrue.

反模式

下面展示一下坏的使用方式代码:

class FooActor extends Actor {
  def receive = {
    case (x:FooRequest) => {
      val x = database.runQuery("SELECT * FROM foo WHERE ", x)
      val y = redis.get(x.fookey)
      sender ! computeResponse(x,y)
    }
  }
}
<p>

在别的地方,FooActor是如下使用:

val fooResult: Future[Any] = fooActor ? FooRequest(...)
<p>

关键需要注意到FooActor并没有任何可变状态,FooActor内部没有任何属性字段等,它只是接受一个消息,这个FooActor继承了actor,Akka没有选择地以单线程方式运行这段代码。

再比较另外一种写法,下面代码没有使用Actor,而是使用了Future

class FooRequester(system: ActorSystem) {
  import system.dispatcher

  def fooResult(x: FooRequest): Future[FooResponse] = Future {
    val x = database.runQuery("SELECT * FROM foo WHERE ", x)
    val y = redis.get(x.fookey)
    computeResponse(x,y)
  }
}
<p>

调用代码如下:

val fooResult: Future[FooResponse] = myFooRequester.fooResult(FooRequest(...))
<p>

这段使用future而不是Actor的好处是大大提高并发性。

如果我们使用Actor,考虑以下并发使用场景:

val r1 = fooActor ? request1
val r2 = fooActor ? request2
for {
  result1 <- r1
  result2 <- r2
} yield (combination(result1.asInstanceOf[FooResponse], result2.asInstanceOf[FooResponse]))
<p>

这段代码future r1和r2原则上应该是并行运行,它们应该是单独计算的,但是因为fooActor是单线程的缘故,这种计算也是单线程的,相反,下面使用Future的计算是多线程的:

val r1 = myFooRequester.fooResult(request1)
val r2 = myFooRequester.fooResult(request2)
for {
  result1 <- r1
  result2 <- r2
} yield (combination(result1, result2))
<p>

第二个好处是安全,后者使用使用typed actors实现的。

不熟悉Akka future的人可能会问,上面代码调用为什么不能使用如下方式?

for {
  result1 <- myFooRequester.fooResult(request1)
  result2 <- myFooRequester.fooResult(request2)
} yield (combination(result1, result2))
<p>

前者能够并行运行,而后者只能序列化串行运行,后者代码等同于:

myFooRequester.fooResult(request1).flatMap( result1 =>
  myFooRequester.fooResult(request2).flatMap( result2 =>
    combination(result1, result2)
  )
)
<p>

前者很清晰,myFooRequester.fooResult(request2) 只有在result1可用时才会取值。

该文还指出使用多个Actor然后放在router后面,这样做增加了复杂性。

该文通过代码展示如何使用Actor实现状态改变,这是因为Actor单线程操作状态的原因。

还通过代码展示了Actor作为消息端点的使用,最好的案例是Spray Routing。

更多可参考原文

                   

6
banq
2015-01-04 10:04

个人观点:Actor适合有状态修改+并发的场合,如果没有可变状态修改,正如文中所说,是反模式,如果有状态需要同时修改,为避免锁等其他堵塞模式使用,这时使用Actor是合适的。

DDD中聚合实体根中有状态,同时为应付并发请求对实体内状态修改,这时使用Actor模型合适的。

原文标题改为:不要将Actors用于并行,可能更好些,并发的含义中有共享状态争夺的意思。

[该贴被banq于2015-01-04 10:55修改过]

pinghe
2015-01-20 16:23

future对于单机是合适的。但在集群环境下,对于查询,actor使用应该基于路由+多实例方式,因为actor位置透明,利于集群扩展。