不要将Actors用于并发编程

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

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


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

关键需要注意到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)
}
}

调用代码如下:


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

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

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


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

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


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

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

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


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

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


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

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

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

该文通过代码展示如何使用Actor实现状态改变,这是因为Actor单线程操作状态的原因。
还通过代码展示了Actor作为消息端点的使用,最好的案例是Spray Routing。

更多可参考原文

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

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

原文标题改为:不要将Actors用于并行,可能更好些,并发的含义中有共享状态争夺的意思。
[该贴被banq于2015-01-04 10:55修改过]

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