不要將Actors用於併發程式設計

banq發表於2015-01-04
將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 class="indent">

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

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

關鍵需要注意到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 class="indent">


呼叫程式碼如下:

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


這段使用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 class="indent">


這段程式碼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 class="indent">


第二個好處是安全,後者使用使用typed actors實現的。

不熟悉Akka future的人可能會問,上面程式碼呼叫為什麼不能使用如下方式?

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

前者能夠並行執行,而後者只能序列化序列執行,後者程式碼等同於:

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


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

該文還指出使用多個Actor然後放在router後面,這樣做增加了複雜性。

該文透過程式碼展示如何使用Actor實現狀態改變,這是因為Actor單執行緒操作狀態的原因。
還透過程式碼展示了Actor作為訊息端點的使用,最好的案例是Spray Routing。

更多可參考原文

相關文章